Skip to content

Revert "Fix workspace group drag drop intent (#6532)" — crashes app-host on main - #6713

Merged
azooz2003-bit merged 1 commit into
mainfrom
revert-6532-dragdrop-crash
Jun 23, 2026
Merged

azooz2003-bit merged 1 commit into
mainfrom
revert-6532-dragdrop-crash

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 23, 2026 •

Copy link
Copy Markdown
Contributor

Reverts #6532.

Why

#6532 broke main CI. It was the only commit between the last all-green run and two consecutive red runs:

  • Last green: run for 18e4d64a (14:09) — all 4 app-host shards passed clean.
  • After Fix workspace group drag drop intent #6532 merged (5cf0557e, 22:34): app-host crashes on every run.
    • 5cf0557e → ❌ app-host unit tests (shard 1/4)
    • 96221935 (HEAD) → ❌ app-host unit tests (shard 4/4)

Crash

The app-host segfaults during UI-rendering tests:

[Invalid Configuration] NSHostingView is being laid out reentrantly while rendering its SwiftUI content...
EXC_BAD_ACCESS (SIGSEGV) — KERN_INVALID_ADDRESS at 0x20
Restarting after unexpected exit, crash, or test timeout  (×27)

The crash is nondeterministic (hit shard 1/4 in one run, 4/4 in the next), so it takes down a different batch of otherwise-unrelated tests each run (CLINotifyProcessIntegrationRegressionTests, browser, keyboard-settings) — which is why it first looked flaky. The reentrant-layout-then-segfault points at the new sidebar drop-overlay rendering (SidebarWorkspaceReorderDropOverlay, +663 lines in ContentView.swift).

Reverting to unblock main. @azooz2003-bit can re-land #6532 with the reentrant SwiftUI layout fixed (likely a state write during body/layout in the new drop overlay — see the snapshot-boundary / no-state-mutation-in-body rules in CLAUDE.md).

🤖 Generated with Claude Code


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


Note

Medium Risk
Large behavioral rollback in sidebar reorder/group drag UX with substantial deleted code; risk is regressing #6532 fixes while fixing CI crashes, not new security surface.

Overview
Reverts #6532 to restore stable sidebar drag/drop and unblock app-host CI after reentrant SwiftUI layout / segfaults tied to the new overlay.

Removed: The SidebarWorkspaceReorderDropResolver stack in CmuxFoundation (hit testing, group/top-level scopes, cross-window plans), dropIndicatorScope on SidebarDragState, explicitGroupId reorder paths, bottom-edge drop lines, and the AppKit SidebarWorkspaceReorderDropOverlay path in ContentView (frame collection bridge, overlay-driven commit). Related coordinator tests for explicit group drops go with it.

Restored: Per-row SidebarTabDropDelegate / SwiftUI onDrop, simpler SidebarDropPlanner and indicator predicates, and reorder logic without scoped overlay planning.

Kept from the reverted work: SidebarWorkspaceGroupHeaderDropDelegate (center drop on a group header adds a workspace to the group; edges still reorder), plus WorkspaceGroupCoordinator fixes that expand a collapsed group when the focused workspace is added or created inside it.

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


Summary by cubic

Reverts the workspace group drag/drop intent from #6532 to stop app-host segfaults and unblock main CI. Restores the previous sidebar reorder behavior and removes the SwiftUI drop overlay that triggered reentrant layout.

  • Bug Fixes
    • Removed the overlay-based reorder system and resolver in CmuxFoundation and app code (SidebarWorkspaceReorderDropOverlay, resolver/types), eliminating reentrant SwiftUI layout that crashed tests.
    • Restored simple top-edge drop indicator and planner; removed group-scoped/bottom indicator logic and related state.
    • Simplified reorder paths in WorkspaceReorderCoordinator and TabManager (no explicit group-scoped drag path); restored legal insertion range API shape.
    • Added SidebarWorkspaceGroupHeaderDropDelegate to handle center-drops on a group header to add to that group; auto-expands a collapsed group when creating and selecting a new workspace.
    • Updated tests to match revert (removed overlay hit-testing; converted predicate/planner tests to XCTest) and adjusted file-length budgets.

Written for commit a8d37a2. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved group auto-expansion behavior when creating or adding workspaces to groups, ensuring the group expands even when the action completes before group state updates.
    • Fixed drop indicator positioning logic for cleaner, more predictable drag-and-drop feedback during workspace reordering.
  • Refactor

    • Streamlined internal drag-and-drop infrastructure for workspace grouping, simplifying drop handling and cross-window interactions.

@vercel

vercel Bot commented Jun 23, 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 23, 2026 11:29pm
cmux-staging Building Building Preview, Comment Jun 23, 2026 11:29pm

@coderabbitai

coderabbitai Bot commented Jun 23, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Removes the NSView-based workspace reorder drop overlay infrastructure (resolver, plan, request, target, pending-drop, and bridge types). Replaces it with a SwiftUI-native tabDropDelegateFactory pattern throughout sidebar rows and group headers. Introduces SidebarWorkspaceGroupHeaderDropDelegate for center/edge group-header drops. Simplifies SidebarDropPlanner, SidebarTabDropIndicatorPredicate, WorkspaceReorderCoordinator (removing explicitGroupId), and ordering helpers. Adds auto-expand on group membership. Migrates affected tests from Swift Testing to XCTest.

Changes

Sidebar drag/drop system replacement

Layer / File(s) Summary
Deleted overlay infrastructure and public types
Sources/SidebarWorkspaceReorderPendingDrop.swift
Eliminates the NSView-based reorder overlay layer: removes SidebarWorkspaceReorderDropResolver, DropPlan, DropRequest, DropTarget, DropOverlay, DropView, DropOverlayTarget, DropOverlayTargetBridge, PendingDrop, GroupLayout, GroupSnapshot, HitContext, RootTarget, WorkspaceSnapshot, DropIndicatorScope, DropAction, and SidebarWorkspaceDropTargetWriters.
SidebarDropPlanner and SidebarTabDropIndicatorPredicate simplification
Packages/macOS/CmuxFoundation/.../SidebarDropIndicator.swift, SidebarDropPlanner.swift, SidebarTabDropIndicatorPredicate.swift
SidebarDropPlanner.indicator(...) inlines no-op suppression unconditionally and removes the suppressesNoOp overload. topVisible(...) rewrites edge matching to index-based predecessor comparison; bottomVisible(...) removed; emptyAreaTopVisible(...) drops indicatorScope. Sendable conformance removed from SidebarDropEdge and SidebarDropIndicator.
SidebarDragState scope → boolean
Packages/macOS/CmuxSidebar/.../SidebarDragState.swift
Removes dropIndicatorScope property and setDropIndicator(_:scope:) overload; setDropIndicator(_:usesTopLevelRows:) now sets dropIndicatorUsesTopLevelRows directly.
WorkspaceReorderCoordinator: remove explicitGroupId and simplify inference
Packages/macOS/CmuxWorkspaces/.../WorkspaceReorderCoordinator.swift, WorkspacesModel+GroupInvariants.swift
Removes explicitGroupId from reorderWorkspace, reorderSidebarWorkspace, sidebarReorderLegalInsertionRange, and applyDragInferredGroupMembership. Removes explicitGroupWorkspaceReorderPlan. reorderTopLevelWorkspaceItem now requires fromIndex != clampedTarget. Membership inference sets tab.groupId directly. assignGroup no longer triggers group expansion.
WorkspacesModel ordering helpers
Packages/macOS/CmuxWorkspaces/.../WorkspacesModel+Ordering.swift
Promoted-workspace insertion simplified to anchor+1. sidebarTopLevelPinnedWorkspaceIds loses promotingWorkspaceId. clampedTopLevelReorderIndex loses promotingWorkspaceId and clamps from the simplified pinned set.
TabManager API surface
Sources/TabManager.swift
sidebarReorderLegalInsertionRange and reorderSidebarWorkspace drop the explicitGroupId parameter with forwarding calls updated accordingly.
WorkspaceGroupCoordinator: auto-expand group on membership
Packages/macOS/CmuxWorkspaces/.../WorkspaceGroupCoordinator.swift
createWorkspaceInGroup and addWorkspaceToGroup now set group.isCollapsed = false when the affected workspace is selected and the group is collapsed.
SidebarWorkspaceGroupHeaderDropDelegate introduction
Sources/SidebarWorkspaceGroupHeaderDropDelegate.swift
Introduces SidebarWorkspaceGroupHeaderDropZone, SidebarWorkspaceGroupHeaderCenterDropAction, and SidebarWorkspaceGroupHeaderDropPolicy. Implements the drop delegate routing center-zone drops to tabManager.addWorkspaceToGroup and consuming or delegating edge drops.
Group header metrics and view rewiring
Sources/SidebarWorkspaceGroupHeaderMetrics.swift, SidebarWorkspaceGroupHeaderView.swift
Removes drop-indicator inset and dropTargetHeight metrics. SidebarWorkspaceGroupHeaderView replaces bottomDropIndicatorVisible with tabDropDelegateFactory closure and wires .onDrop via the factory.
VerticalTabsSidebar group header wiring
Sources/VerticalTabsSidebar+WorkspaceGroups.swift
Removes bottomDropIndicatorVisible computation; adds tabDropDelegateFactory closure constructing SidebarWorkspaceGroupHeaderDropDelegate instances from rowHeight; updates SidebarWorkspaceGroupHeaderView initializer.
ContentView: SwiftUI drop wiring and TabItemView factory pattern
Sources/ContentView.swift
Adds SidebarWorkspaceTopDropIndicator view. workspaceScrollContent delegates to workspaceRows(renderContext:). TabItemView loses bottomDropIndicatorVisible, gains tabDropDelegateFactory. SidebarEmptyArea always receives tabDropDelegate. updateDropIndicator splits cross-window path. dropExited clears indicator on target match. performDrop uses defer cleanup.
Xcode project wiring
cmux.xcodeproj/project.pbxproj
Adds SidebarWorkspaceGroupHeaderDropDelegate and related sidebar group sources; removes reorder overlay sources and test targets from build phases, file references, and group children.
Test updates: XCTest migration and removed tests
cmuxTests/SidebarTabDropIndicatorPredicateTests.swift, cmuxTests/SidebarWorkspaceDropPlannerTests.swift, cmuxTests/SidebarWorkspaceGroupHeaderMetricsTests.swift, cmuxTests/WorkspaceGroupTests.swift, Packages/macOS/CmuxWorkspaces/Tests/.../WorkspaceCoordinatorTests.swift
Migrates Swift Testing suites to XCTest throughout. Removes SidebarWorkspaceReorderDropOverlayHitTestingTests, groupScopedHeaderBottomIndicatorAlignsWithMemberRows, and two workspace promotion tests. Updates WorkspaceGroupTests assertions to topVisible-only.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • manaflow-ai/cmux#6532: Directly inverse: introduces explicitGroupId propagation through TabManager/ContentView via a new drop intent policy, while this PR removes that entire path.
  • manaflow-ai/cmux#6625: Touches the same tabDropDelegateFactory/rowHeight wiring in ContentView, SidebarWorkspaceGroupHeaderView, and SidebarWorkspaceGroupHeaderMetrics.dropTargetHeight that this PR rewrites.
  • manaflow-ai/cmux#6226: Introduces the SidebarDragState extraction with setDropIndicator(_:usesTopLevelRows:) that this PR further simplifies by removing the scope:-based overload.

Suggested reviewers

  • austinywang

Poem

🐇 Hoppity-hop through the drop zone we go,
Old overlays gone, now a factory in tow!
explicitGroupId? No need, friend, begone!
The predicate sings a simpler top-visible song.
Groups expand, workspaces land where they're seen,
The cleanest drag-drop the sidebar has been! ✨


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 Algorithmic Complexity ❌ Error PR restores O(n²) algorithmic complexity: topVisible() at SidebarTabDropIndicatorPredicate.swift:19 calls tabIds.firstIndex(of:) per-row in ForEach loops handling 1000+ workspaces during every drag... Replace Array.firstIndex(of:) with pre-computed index lookup from render context, or restructure predicate to avoid per-row linear scans (preferably O(1) using Set/Dictionary lookups passed from parent).
Docstring Coverage ⚠️ Warning Docstring coverage is 21.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (23 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: reverting PR #6532 due to app-host crashes. It is specific and directly related to the changeset.
Description check ✅ Passed The description comprehensively covers the revert context, root cause analysis, and impact. It includes the CI failure details, crash symptoms, and mentions the new suspected source of the issue, meeting the template's Summary and Testing sections with depth.
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 Revert removes 8 properly-Sendable value types and 700+ lines of drop-overlay code. No new actor isolation mistakes introduced; SidebarDragState remains @MainActor, removed types were immutable.
Cmux Swift Blocking Runtime ✅ Passed The revert PR removes problematic new overlay code from PR #6532 (including SidebarWorkspaceReorderDropOverlay, SidebarWorkspaceReorderDropView) without introducing or materially expanding blocking...
Cmux Browser Automation Off-Main ✅ Passed This PR modifies only workspace/sidebar UI and reordering logic. It does not touch browser automation files (TerminalController.swift or ControlCommandExecutionPolicy.swift) or socket worker method...
Cmux Expensive Synchronous Load ✅ Passed Revert commit removes segfaulting code; no expensive agent-history sync loads added to main paths. RestorableAgentSessionIndex.load() properly called off-main via Task.detached.
Cmux Cache Substitution Correctness ✅ Passed This revert removes problematic code from PR #6532 and restores the prior working version. The restored code uses fresh reads from model state (tabs/groups) without any cache substitution; no persi...
Cmux No Hacky Sleeps ✅ Passed This PR contains only Swift code changes. The custom check explicitly excludes Swift timing primitives: "Swift sleeps are covered by cmux Swift blocking runtime". No TypeScript, JavaScript, shell...
Cmux Swift Concurrency ✅ Passed This is a revert commit that removes PR #6532 and restores prior code. It does not introduce or expand legacy async patterns; it removes the problematic code added by #6532 that caused the reentran...
Cmux Swift @Concurrent ✅ Passed This revert PR introduces no @concurrent annotation violations. Existing async functions (SidebarFontSizeProvider.loadFromGhosttyConfig and TabManager waitFor* functions) remain unchanged and prope...
Cmux Swift File And Package Boundaries ✅ Passed PR is a pure revert of #6532 (segfaulting commit) to unblock main CI. Restores code to previous state; no new boundary violations introduced.
Cmux Swiftpm Lockfiles ✅ Passed PR includes all required cmux-owned Package.resolved files, root Xcode Package.resolved, and no .gitignore files ignore Package.resolved, complying with swiftpm-package-resolved.md policy.
Cmux Swift Logging ✅ Passed All cmuxDebugLog calls are properly guarded with #if DEBUG. No unguarded print/debugPrint/dump/NSLog found. Logger declarations are pre-existing code not added by this revert PR.
Cmux User-Facing Error Privacy ✅ Passed This revert PR restores deleted code without introducing new user-facing error messages. All user-visible strings are properly localized using String(localized:defaultValue:). No sensitive details...
Cmux Full Internationalization ✅ Passed This revert restores files from a previous safe state and fully restores Resources/Localizable.xcstrings (210K+ lines) including all existing translations; it does not introduce new user-facing tex...
Cmux Swiftui State Layout ✅ Passed This revert PR removes the problematic SidebarWorkspaceReorderDropOverlay.swift and SidebarWorkspaceReorderDropView.swift files that caused reentrant NSHostingView layout crashes through state muta...
Cmux Architecture Rethink ✅ Passed Revert removes problematic NSViewRepresentable drag-overlay code that caused reentrant SwiftUI layout crashes. Does not introduce timing repairs, delayed dispatch, locks, observers, or split UI lif...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed This revert removes NSView and NSViewRepresentable code (SidebarWorkspaceReorderDropView, SidebarWorkspaceReorderDropOverlay) that were added in PR #6532, with no new window-related code introduced...
Cmux Source Artifacts ✅ Passed All changes are hand-written source files, tests, and build config; no build artifacts, generated logs, caches, or temp folders are added. The revert properly removes problematic code without intro...
Cmux No Test Or Debug Seam In Production Source ✅ Passed This revert PR restores pre-existing test seams and debug diagnostics (like ForTesting methods, debugPinnedWorkspaceLoadIds, customSidebarsDirectoryOverrideForTesting) that were removed by PR...
Cmux No Ambient Global State ✅ Passed Revert PR introduces zero new top-level functions, mutable vars, or singletons; all restored code pre-exists and was approved before PR #6532.
✨ 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 revert-6532-dragdrop-crash

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.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a8d37a2. Configure here.

Comment thread Sources/ContentView.swift
// Hover updates and drag-end own indicator changes.
if dragState.dropIndicator?.tabId == targetTabId {
dragState.clearDropIndicator()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Drop exit clears indicator early

Medium Severity

SidebarTabDropDelegate again clears the active drop indicator in dropExited when dropIndicator.tabId matches the row delegate. SwiftUI often emits spurious row exits during drag-driven relayout, so the line can vanish while the pointer is still over a valid target until the next dropUpdated.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a8d37a2. Configure here.

}
if tab.groupId != inferred {
model.assignGroup(workspaceId: workspaceId, groupId: inferred)
tab.groupId = inferred

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Drag join hides selected workspace

Medium Severity

After a drag-driven reorder, group membership is applied by setting tab.groupId directly, and assignGroup no longer expands collapsed groups for the focused workspace. If the dragged tab is already selected and lands in a collapsed group, selectedTabId does not change, so the sidebar can keep the group folded and hide the active workspace.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a8d37a2. Configure here.

@greptile-apps

greptile-apps Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR reverts #6532 ("Fix workspace group drag drop intent") to unblock main CI after that change caused reentrant SwiftUI layout crashes (NSHostingView segfault during app-host unit-test shards). The revert removes ~3,300 lines of new drop-overlay machinery (SidebarWorkspaceReorderDropOverlay, SidebarWorkspaceReorderDropResolver, and related types) and restores the previous group-header drop path.

  • Restores SidebarWorkspaceGroupHeaderDropDelegate (pre-Fix workspace group drag drop intent #6532 group-header drop logic) and the simpler SidebarTabDropIndicatorPredicate without the scope-aware bottomVisible branch.
  • Removes SidebarWorkspaceReorderDropIndicatorScope, SidebarWorkspaceReorderDropOverlay, and ten other new files introduced by Fix workspace group drag drop intent #6532.
  • Adds two new blocks in WorkspaceGroupCoordinator that expand a collapsed group when the selected workspace is created inside it or moved into it — these are not part of the pure revert and address a separate collapsed-group visibility bug.

Confidence Score: 4/5

Safe to merge — the crash-inducing overlay is fully removed and the restored drop-delegate path is the previously stable implementation.

The bulk of the change is a clean, mechanical revert of the 663-line SwiftUI drop overlay causing reentrant layout segfaults. The restored code was working before #6532 landed. Two new blocks in WorkspaceGroupCoordinator patch a collapsed-group visibility bug correctly but leave a host-sequencing ordering issue unaddressed. The SidebarWorkspaceGroupHeaderView Equatable conformance omits tabDropDelegateFactory, leaving a narrow stale-delegate window on membership-only changes outside a drag.

WorkspaceGroupCoordinator.swift (new timing-patch blocks) and SidebarWorkspaceGroupHeaderView.swift (Equatable omits tabDropDelegateFactory)

Important Files Changed

Filename Overview
Sources/ContentView.swift Removes 663-line SidebarWorkspaceReorderDropOverlay insertion and restores earlier, simpler workspace-reorder drop logic; adds back SidebarWorkspaceTopDropIndicator struct inline. Changes are a correct revert.
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift Adds two new auto-expand blocks (not part of the revert) that expand a collapsed group when a selected workspace is created inside it or assigned to it, patching a host-sequencing race.
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift Reverts explicitGroupId parameter and associated planning logic introduced by #6532; restores simpler reorder path.
Sources/SidebarWorkspaceGroupHeaderDropDelegate.swift Restored pre-#6532 group-header drop delegate with zone-split logic (center = add-to-group, edge = reorder); correct and MainActor-isolated.
Sources/SidebarWorkspaceGroupHeaderView.swift Replaces bottomDropIndicatorVisible with tabDropDelegateFactory; the factory closure is intentionally excluded from Equatable comparison, leaving a narrow stale-delegate window on membership-only changes.
.github/swift-file-length-budget.tsv Budget correctly updated: ContentView.swift reduced, deleted files removed from tracking, WorkspaceReorderCoordinator budget dropped to reflect post-revert line count.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Host as TabManager / Host
    participant GC as WorkspaceGroupCoordinator
    participant Model as WorkspacesModel
    participant Sidebar as VerticalTabsSidebar (SwiftUI)

    Note over Host,Sidebar: Before revert (#6532 path — crashes)
    Host->>GC: createWorkspace(inGroup:)
    GC->>Model: assignGroup + normalizeContiguity
    Sidebar-->>Sidebar: SidebarWorkspaceReorderDropOverlay body (reentrant layout → SIGSEGV)

    Note over Host,Sidebar: After revert (this PR)
    Host->>GC: createWorkspace(inGroup:select:)
    GC->>Model: assignGroup
    GC->>Model: "expand group if select && isCollapsed [NEW]"
    GC->>Model: normalizeWorkspaceGroupContiguity
    GC-->>Host: newWorkspace

    Host->>GC: addWorkspaceToGroup(workspaceId:groupId:)
    GC->>Model: assignGroup
    GC->>Model: "expand group if selectedTabId==workspaceId && isCollapsed [NEW]"
    GC->>Model: normalizeWorkspaceGroupContiguity

    Sidebar->>Sidebar: SidebarWorkspaceGroupHeaderDropDelegate.performDrop
    Sidebar->>Host: addWorkspaceToGroup via tabManager
    Sidebar->>Sidebar: clearDropState (dragState + autoScroll)
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 Host as TabManager / Host
    participant GC as WorkspaceGroupCoordinator
    participant Model as WorkspacesModel
    participant Sidebar as VerticalTabsSidebar (SwiftUI)

    Note over Host,Sidebar: Before revert (#6532 path — crashes)
    Host->>GC: createWorkspace(inGroup:)
    GC->>Model: assignGroup + normalizeContiguity
    Sidebar-->>Sidebar: SidebarWorkspaceReorderDropOverlay body (reentrant layout → SIGSEGV)

    Note over Host,Sidebar: After revert (this PR)
    Host->>GC: createWorkspace(inGroup:select:)
    GC->>Model: assignGroup
    GC->>Model: "expand group if select && isCollapsed [NEW]"
    GC->>Model: normalizeWorkspaceGroupContiguity
    GC-->>Host: newWorkspace

    Host->>GC: addWorkspaceToGroup(workspaceId:groupId:)
    GC->>Model: assignGroup
    GC->>Model: "expand group if selectedTabId==workspaceId && isCollapsed [NEW]"
    GC->>Model: normalizeWorkspaceGroupContiguity

    Sidebar->>Sidebar: SidebarWorkspaceGroupHeaderDropDelegate.performDrop
    Sidebar->>Host: addWorkspaceToGroup via tabManager
    Sidebar->>Sidebar: clearDropState (dragState + autoScroll)
Loading

Reviews (1): Last reviewed commit: "Revert "Fix workspace group drag drop in..." | Re-trigger Greptile

Comment on lines +155 to 162
if select,
let idx = model.workspaceGroups.firstIndex(where: { $0.id == groupId }),
model.workspaceGroups[idx].isCollapsed {
model.workspaceGroups[idx].isCollapsed = false
}
model.normalizeWorkspaceGroupContiguity()
host.workspaceOrderDidChange(movedWorkspaceIds: [newWorkspace.id])
return newWorkspace

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 Timing patch leaves host sequencing bug representable

The comment explains the root problem: the host's selectedTabId auto-expand hook fires before assignGroup completes, so group membership isn't visible when expansion is first attempted. These two new blocks compensate by imperatively re-expanding inside the coordinator after assignment. This is the pattern flagged by the architectural-rethink rule — a timing repair that leaves the bad host-sequencing invariant in place. The coordinator now silently absorbs responsibility for a lifecycle ordering guarantee that belongs to the host's workspace-creation call sequence. If the host ever changes the order of selectedTabId assignment relative to group assignment, this second expansion could fire redundantly or conflict with a future expansion guard. Consider documenting the exact caller-sequence constraint this relies on, and track fixing the host's ordering as follow-up work before re-landing #6532.

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

Comment on lines 37 to 41
lhs.rowSpacing == rhs.rowSpacing &&
lhs.isFirstRow == rhs.isFirstRow &&
lhs.isBeingDragged == rhs.isBeingDragged &&
lhs.topDropIndicatorVisible == rhs.topDropIndicatorVisible &&
lhs.bottomDropIndicatorVisible == rhs.bottomDropIndicatorVisible
lhs.topDropIndicatorVisible == rhs.topDropIndicatorVisible
}

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 tabDropDelegateFactory excluded from Equatable — stale delegate on membership-only changes

The Equatable conformance compares topDropIndicatorVisible and isBeingDragged but not tabDropDelegateFactory. The factory closure captures workspaceGroupIdByWorkspaceId from renderContext. If workspace group membership changes while no drag indicator is active (e.g., a socket command reassigns a workspace to a different group while the sidebar is idle), topDropIndicatorVisible and isBeingDragged are both false and unchanged, so SwiftUI's equatable optimization skips re-rendering the row. The .onDrop(delegate:) modifier then holds a stale delegate with the old workspaceGroupIdByWorkspaceId snapshot, which could route a subsequent drag-drop to the wrong group. In practice this window is narrow, but worth documenting or closing by including a version counter or checksum of the drop-relevant inputs in the equality check.

Rule Used: Flag SwiftUI changes that can cause stale state, b... (source)

@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: 6

Caution

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

⚠️ Outside diff range comments (1)
Sources/ContentView.swift (1)

15672-15734: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Don’t treat unchanged index as no-op when group membership changes.

After removing explicitGroupId, a drop can keep the same numeric targetIndex but still need a top-level ⇄ group membership transition. The guard on Line 15715 skips reorderSidebarWorkspace(...), so the coordinator never gets a chance to apply the inferred group change. Include the inferred target group/current group in the no-op check, or move the no-op decision into the coordinator where membership inference is authoritative.

🤖 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 `@Sources/ContentView.swift` around lines 15672 - 15734, The guard statement
checking `fromIndex != targetIndex` treats a drop as a no-op and returns early
without calling `reorderSidebarWorkspace()`, but this fails to account for group
membership transitions that can occur even when the numeric index remains
unchanged. Modify the no-op guard to also check whether the inferred target
group membership differs from the current group membership by computing the
target group for the drop location and comparing it against the current group,
or alternatively move the no-op decision logic into the coordinator where group
membership inference is authoritative so that membership changes are properly
applied regardless of index position.
🤖 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/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarTabDropIndicatorPredicate.swift`:
- Around line 18-24: The predicate containing the guard statement that checks
`tabIds.firstIndex(of: tabId)` performs a full linear scan over the tabIds
collection for every sidebar row render, causing O(n²) performance. Instead of
scanning within the predicate, modify the method signature to accept a
precomputed index lookup (such as a dictionary mapping tabId to its index
position) as a parameter from the render context. Replace the
`tabIds.firstIndex(of: tabId)` call with an O(1) lookup from this precomputed
dictionary, and update the call sites to pass this lookup when invoking the
predicate.

In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift`:
- Around line 274-283: The guard statement at the beginning of the method
prevents execution from reaching the model.assignGroup call when fromIndex
equals clampedTarget. For grouped workspaces being promoted, the function should
still clear the groupId even when the top-level indices are unchanged, since
sidebarTopLevelWorkspaceIds can synthesize the promoted row at the same index.
Modify the guard condition to allow the function to continue when
promotesGroupedWorkspace is true and fromIndex equals clampedTarget, so the
groupId clearing logic in model.assignGroup still executes while skipping the
unnecessary topLevelIds reordering.

In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel`+Ordering.swift:
- Around line 97-115: The function clampedTopLevelReorderIndex relies on
sidebarTopLevelPinnedWorkspaceIds which recomputes the pinned set from the
non-promoted top-level view, causing promoted children to not be recognized as
pinned. To fix this, modify clampedTopLevelReorderIndex to accept the dragged
workspace ID as a parameter (the workspace being moved), and when determining
pin tier constraints, check the authoritative pin state of that workspace
directly from the model state (checking both workspace and group isPinned
properties) rather than relying solely on the computed pinnedIds set. This
ensures the promoted child's pin state is preserved during reordering.

In
`@Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift`:
- Line 213: The test methods contain multiple instances of try! `#require`(...) at
lines 213, 244, 263, 278, and 317 which cause forced crashes instead of proper
test failure assertions. For each test method containing this pattern, add
throws to the test method signature and replace try! `#require`(...) with try
`#require`(...) so that requirement failures are properly reported as test
assertions rather than hard crashes.

In `@Sources/ContentView.swift`:
- Around line 10620-10635: The top drop zone overlay is gated on both
dragState.draggedTabId != nil and firstWorkspaceId existing, which prevents the
SidebarTabDropDelegate from mounting during foreign (cross-window) drags. Remove
the dragState.draggedTabId != nil check from the if condition so the overlay
remains mounted whenever firstWorkspaceId exists, allowing the delegate to
properly validate the payload and handle cross-window drags through
activateForeignDragIfNeeded().
- Around line 12308-12321: Remove the delayed failsafe mechanism in the
requestClearSoon function that uses DispatchQueue.main.asyncAfter with
SidebarDragFailsafePolicy.clearDelay. Instead of scheduling a delayed clear via
DispatchWorkItem, integrate the clearing logic directly into the drag lifecycle
state machine or trigger the onRequestClear callback from a concrete monitor
event that represents the actual end of the drag operation. This eliminates the
timing-based repair path and makes the state transition explicit and predictable
rather than relying on a delayed dispatch that can hide lifecycle races.

---

Outside diff comments:
In `@Sources/ContentView.swift`:
- Around line 15672-15734: The guard statement checking `fromIndex !=
targetIndex` treats a drop as a no-op and returns early without calling
`reorderSidebarWorkspace()`, but this fails to account for group membership
transitions that can occur even when the numeric index remains unchanged. Modify
the no-op guard to also check whether the inferred target group membership
differs from the current group membership by computing the target group for the
drop location and comparing it against the current group, or alternatively move
the no-op decision logic into the coordinator where group membership inference
is authoritative so that membership changes are properly applied regardless of
index position.
🪄 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: f002dc34-2743-4dde-bd12-efaff43cb391

📥 Commits

Reviewing files that changed from the base of the PR and between 9622193 and a8d37a2.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (39)
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarDropIndicator.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarDropPlanner.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarTabDropIndicatorPredicate.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropAction.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropIndicatorScope.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropPlan.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropRequest.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropTarget.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderGroupLayout.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderGroupSnapshot.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderHitContext.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderRootTarget.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderWorkspaceSnapshot.swift
  • Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Drag/SidebarDragState.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel+GroupInvariants.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel+Ordering.swift
  • Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift
  • Sources/ContentView.swift
  • Sources/SidebarWorkspaceDropTargetWriters.swift
  • Sources/SidebarWorkspaceGroupHeaderDropDelegate.swift
  • Sources/SidebarWorkspaceGroupHeaderMetrics.swift
  • Sources/SidebarWorkspaceGroupHeaderView.swift
  • Sources/SidebarWorkspaceReorderDropOverlay.swift
  • Sources/SidebarWorkspaceReorderDropOverlayTarget.swift
  • Sources/SidebarWorkspaceReorderDropOverlayTargetBridge.swift
  • Sources/SidebarWorkspaceReorderDropView.swift
  • Sources/SidebarWorkspaceReorderPendingDrop.swift
  • Sources/SidebarWorkspaceTopDropIndicator.swift
  • Sources/TabManager.swift
  • Sources/VerticalTabsSidebar+WorkspaceGroups.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/SidebarTabDropIndicatorPredicateTests.swift
  • cmuxTests/SidebarWorkspaceDropPlannerTests.swift
  • cmuxTests/SidebarWorkspaceGroupHeaderMetricsTests.swift
  • cmuxTests/SidebarWorkspaceReorderDropOverlayHitTestingTests.swift
  • cmuxTests/WorkspaceGroupTests.swift
💤 Files with no reviewable changes (21)
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderGroupSnapshot.swift
  • Sources/SidebarWorkspaceTopDropIndicator.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropAction.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropPlan.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropRequest.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderWorkspaceSnapshot.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropTarget.swift
  • Sources/SidebarWorkspaceReorderPendingDrop.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderGroupLayout.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderHitContext.swift
  • Sources/SidebarWorkspaceReorderDropOverlay.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropIndicatorScope.swift
  • Sources/SidebarWorkspaceReorderDropView.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderRootTarget.swift
  • cmuxTests/SidebarWorkspaceGroupHeaderMetricsTests.swift
  • Sources/SidebarWorkspaceReorderDropOverlayTarget.swift
  • Sources/SidebarWorkspaceReorderDropOverlayTargetBridge.swift
  • cmuxTests/SidebarWorkspaceReorderDropOverlayHitTestingTests.swift
  • Sources/SidebarWorkspaceDropTargetWriters.swift
  • Sources/SidebarWorkspaceGroupHeaderMetrics.swift

Comment on lines +18 to +24
guard indicator.edge == .bottom,
let currentIndex = tabIds.firstIndex(of: tabId),
currentIndex > 0
else {
return false
}
return true
return tabIds[currentIndex - 1] == indicator.tabId

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Avoid the per-row full scan in the drop-indicator predicate.

Line 19 runs tabIds.firstIndex(of:) from a predicate called by each sidebar row/header render, so a bottom-edge drag indicator turns the render pass into O(n²) over workspace rows. Pass a precomputed predecessor/index lookup from the render context and make this predicate O(1). As per path instructions, “In SwiftUI/body and drag-drop hit-testing/rendering paths, avoid repeated full scans over scalable collections.”

🤖 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/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarTabDropIndicatorPredicate.swift`
around lines 18 - 24, The predicate containing the guard statement that checks
`tabIds.firstIndex(of: tabId)` performs a full linear scan over the tabIds
collection for every sidebar row render, causing O(n²) performance. Instead of
scanning within the predicate, modify the method signature to accept a
precomputed index lookup (such as a dictionary mapping tabId to its index
position) as a parameter from the render context. Replace the
`tabIds.firstIndex(of: tabId)` call with an O(1) lookup from this precomputed
dictionary, and update the call sites to pass this lookup when invoking the
predicate.

Source: Path instructions

Comment on lines +274 to 283
guard fromIndex != clampedTarget else { return false }

var desiredTopLevelIds = topLevelIds
if fromIndex != clampedTarget {
let movedId = desiredTopLevelIds.remove(at: fromIndex)
desiredTopLevelIds.insert(movedId, at: clampedTarget)
}
if shouldPromoteGroupedWorkspace {
let movedId = desiredTopLevelIds.remove(at: fromIndex)
desiredTopLevelIds.insert(movedId, at: clampedTarget)
if promotesGroupedWorkspace,
let tab = model.tabs.first(where: { $0.id == tabId }),
tab.groupId != nil,
!model.isWorkspaceGroupAnchor(tabId) {
model.assignGroup(workspaceId: tabId, groupId: nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Allow promotion even when the top-level index is unchanged.

For grouped children, sidebarTopLevelWorkspaceIds(promotingWorkspaceId:) can synthesize the promoted row at the same fromIndex; the required change is still clearing groupId. This guard returns before line 283, so dropping a child into its immediate top-level slot is ignored instead of promoting it.

Proposed fix
-        guard fromIndex != clampedTarget else { return false }
+        let shouldPromoteGroupedWorkspace = promotesGroupedWorkspace
+            && model.tabs.contains { tab in
+                tab.id == tabId &&
+                    tab.groupId != nil &&
+                    !model.isWorkspaceGroupAnchor(tabId)
+            }
+        guard fromIndex != clampedTarget || shouldPromoteGroupedWorkspace else { return false }
 
         var desiredTopLevelIds = topLevelIds
-        let movedId = desiredTopLevelIds.remove(at: fromIndex)
-        desiredTopLevelIds.insert(movedId, at: clampedTarget)
-        if promotesGroupedWorkspace,
-           let tab = model.tabs.first(where: { $0.id == tabId }),
-           tab.groupId != nil,
-           !model.isWorkspaceGroupAnchor(tabId) {
+        if fromIndex != clampedTarget {
+            let movedId = desiredTopLevelIds.remove(at: fromIndex)
+            desiredTopLevelIds.insert(movedId, at: clampedTarget)
+        }
+        if shouldPromoteGroupedWorkspace {
             model.assignGroup(workspaceId: tabId, groupId: nil)
         }
🤖 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/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift`
around lines 274 - 283, The guard statement at the beginning of the method
prevents execution from reaching the model.assignGroup call when fromIndex
equals clampedTarget. For grouped workspaces being promoted, the function should
still clear the groupId even when the top-level indices are unchanged, since
sidebarTopLevelWorkspaceIds can synthesize the promoted row at the same index.
Modify the guard condition to allow the function to continue when
promotesGroupedWorkspace is true and fromIndex equals clampedTarget, so the
groupId clearing logic in model.assignGroup still executes while skipping the
unnecessary topLevelIds reordering.

Comment on lines +97 to +115
func sidebarTopLevelPinnedWorkspaceIds() -> Set<UUID> {
let groupsByAnchorId = Dictionary(uniqueKeysWithValues: workspaceGroups.map { ($0.anchorWorkspaceId, $0) })
let tabsById = Dictionary(uniqueKeysWithValues: tabs.map { ($0.id, $0) })
return Set(sidebarTopLevelWorkspaceIds(promotingWorkspaceId: promotingWorkspaceId).filter { id in
topLevelWorkspaceIdIsPinned(id, tabsById: tabsById, groupsByAnchorId: groupsByAnchorId)
})
}

private func promotedTopLevelInsertionIndex(
ids: [UUID],
groupIndex: Int,
promotedIsPinned: Bool,
tabsById: [UUID: Tab],
groupsByAnchorId: [UUID: WorkspaceGroup]
) -> Int {
let desiredIndex = min(groupIndex + 1, ids.count)
let pinnedCount = ids.reduce(into: 0) { count, id in
if topLevelWorkspaceIdIsPinned(id, tabsById: tabsById, groupsByAnchorId: groupsByAnchorId) {
count += 1
return Set(sidebarTopLevelWorkspaceIds().filter { id in
if let group = groupsByAnchorId[id] {
return group.isPinned
}
}
return promotedIsPinned ? min(desiredIndex, pinnedCount) : max(desiredIndex, pinnedCount)
}

private func topLevelWorkspaceIdIsPinned(
_ id: UUID,
tabsById: [UUID: Tab],
groupsByAnchorId: [UUID: WorkspaceGroup]
) -> Bool {
if let group = groupsByAnchorId[id] {
return group.isPinned
}
return tabsById[id]?.isPinned == true
return tabsById[id]?.isPinned == true
})
}

/// Clamps a requested top-level reorder index into the mover's pin tier.
func clampedTopLevelReorderIndex(
forWorkspaceId workspaceId: UUID,
targetIndex: Int,
topLevelIds: [UUID],
promotingWorkspaceId: UUID? = nil
topLevelIds: [UUID]
) -> Int {
let clamped = max(0, min(targetIndex, max(0, topLevelIds.count - 1)))
let pinnedIds = sidebarTopLevelPinnedWorkspaceIds(promotingWorkspaceId: promotingWorkspaceId)
let pinnedIds = sidebarTopLevelPinnedWorkspaceIds()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the promoted child’s pin tier.

When topLevelIds includes a promoted grouped child, sidebarTopLevelPinnedWorkspaceIds() recomputes pinned rows from the non-promoted top-level projection, so the promoted child is never in pinnedIds. A pinned child promoted out of a group is then clamped/planned as unpinned and can land below the pinned boundary.

Proposed fix
-    func sidebarTopLevelPinnedWorkspaceIds() -> Set<UUID> {
+    func sidebarTopLevelPinnedWorkspaceIds(promotingWorkspaceId promotedWorkspaceId: UUID? = nil) -> Set<UUID> {
         let groupsByAnchorId = Dictionary(uniqueKeysWithValues: workspaceGroups.map { ($0.anchorWorkspaceId, $0) })
         let tabsById = Dictionary(uniqueKeysWithValues: tabs.map { ($0.id, $0) })
-        return Set(sidebarTopLevelWorkspaceIds().filter { id in
+        var pinnedIds = Set(sidebarTopLevelWorkspaceIds(promotingWorkspaceId: promotedWorkspaceId).filter { id in
             if let group = groupsByAnchorId[id] {
                 return group.isPinned
             }
             return tabsById[id]?.isPinned == true
-        })
+        })
+        if let promotedWorkspaceId,
+           tabsById[promotedWorkspaceId]?.isPinned == true {
+            pinnedIds.insert(promotedWorkspaceId)
+        }
+        return pinnedIds
     }
 
     /// Clamps a requested top-level reorder index into the mover's pin tier.
@@
-        let pinnedIds = sidebarTopLevelPinnedWorkspaceIds()
+        let pinnedIds = sidebarTopLevelPinnedWorkspaceIds(promotingWorkspaceId: workspaceId)

Also pass the dragged workspace from the coordinator’s top-level planning path:

-        return model.sidebarTopLevelPinnedWorkspaceIds()
+        return model.sidebarTopLevelPinnedWorkspaceIds(promotingWorkspaceId: draggedWorkspaceId)

As per path instructions, correctness-critical UI state must have one authoritative source of truth rather than an unreliable fallback; here the authoritative pin state is the workspace/group model state, including the promoted workspace.

🤖 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/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel`+Ordering.swift
around lines 97 - 115, The function clampedTopLevelReorderIndex relies on
sidebarTopLevelPinnedWorkspaceIds which recomputes the pinned set from the
non-promoted top-level view, causing promoted children to not be recognized as
pinned. To fix this, modify clampedTopLevelReorderIndex to accept the dragged
workspace ID as a parameter (the workspace being moved), and when determining
pin tier constraints, check the authoritative pin state of that workspace
directly from the model state (checking both workspace and group isPinned
properties) rather than relying solely on the computed pinnedIds set. This
ensures the promoted child's pin state is preserved during reordering.

Source: Path instructions

)

let group = try #require(model.workspaceGroups.first(where: { $0.id == groupId }))
let group = try! #require(model.workspaceGroups.first(where: { $0.id == groupId }))

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

Remove try! in tests and restore throwing test signatures.

try! #require(...) at Line 213, Line 244, Line 263, Line 278, and Line 317 introduces forced-crash paths and triggers the current lint errors. Prefer throws test methods with try #require(...) so failures are reported as assertions instead of hard crashes.

Suggested fix pattern
-    func createWorkspaceGroupAdoptsChildrenAndKeepsSectionContiguous() {
+    func createWorkspaceGroupAdoptsChildrenAndKeepsSectionContiguous() throws {
...
-        let group = try! `#require`(model.workspaceGroups.first(where: { $0.id == groupId }))
+        let group = try `#require`(model.workspaceGroups.first(where: { $0.id == groupId }))

Apply the same pattern to the other changed tests at Lines 239, 258, 274, and 311.

Also applies to: 244-244, 263-263, 278-278, 317-317

🧰 Tools
🪛 SwiftLint (0.64.0)

[Error] 213-213: Force tries should be avoided

(force_try)

🤖 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/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift`
at line 213, The test methods contain multiple instances of try! `#require`(...)
at lines 213, 244, 263, 278, and 317 which cause forced crashes instead of
proper test failure assertions. For each test method containing this pattern,
add throws to the test method signature and replace try! `#require`(...) with try
`#require`(...) so that requirement failures are properly reported as test
assertions rather than hard crashes.

Source: Linters/SAST tools

Comment thread Sources/ContentView.swift
Comment on lines +10620 to +10635
.overlay(alignment: .top) {
if dragState.draggedTabId != nil, let firstWorkspaceId = renderContext.workspaceIds.first {
Color.clear
.contentShape(Rectangle())
.frame(height: scrollInsets.top + 8)
.onDrop(of: SidebarTabDragPayload.dropContentTypes, delegate: SidebarTabDropDelegate(
targetTabId: firstWorkspaceId,
tabManager: tabManager,
workspaceGroupIdByWorkspaceId: renderContext.workspaceGroupIdByWorkspaceId,
dragState: dragState,
selectedTabIds: $selectedTabIds,
lastSidebarSelectionIndex: $lastSidebarSelectionIndex,
targetRowHeight: nil,
dragAutoScrollController: dragAutoScrollController
))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep the top drop zone mounted for foreign drags.

Gating this overlay on dragState.draggedTabId != nil means a cross-window drag that first enters the top strip has no delegate mounted to call activateForeignDragIfNeeded(), so dropping above the first workspace can fail until the pointer reaches a row. Mount the drop zone whenever firstWorkspaceId exists and let the delegate validate the payload.

🐛 Proposed fix
-                    if dragState.draggedTabId != nil, let firstWorkspaceId = renderContext.workspaceIds.first {
+                    if let firstWorkspaceId = renderContext.workspaceIds.first {
📝 Committable suggestion

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

Suggested change
.overlay(alignment: .top) {
if dragState.draggedTabId != nil, let firstWorkspaceId = renderContext.workspaceIds.first {
Color.clear
.contentShape(Rectangle())
.frame(height: scrollInsets.top + 8)
.onDrop(of: SidebarTabDragPayload.dropContentTypes, delegate: SidebarTabDropDelegate(
targetTabId: firstWorkspaceId,
tabManager: tabManager,
workspaceGroupIdByWorkspaceId: renderContext.workspaceGroupIdByWorkspaceId,
dragState: dragState,
selectedTabIds: $selectedTabIds,
lastSidebarSelectionIndex: $lastSidebarSelectionIndex,
targetRowHeight: nil,
dragAutoScrollController: dragAutoScrollController
))
}
.overlay(alignment: .top) {
if let firstWorkspaceId = renderContext.workspaceIds.first {
Color.clear
.contentShape(Rectangle())
.frame(height: scrollInsets.top + 8)
.onDrop(of: SidebarTabDragPayload.dropContentTypes, delegate: SidebarTabDropDelegate(
targetTabId: firstWorkspaceId,
tabManager: tabManager,
workspaceGroupIdByWorkspaceId: renderContext.workspaceGroupIdByWorkspaceId,
dragState: dragState,
selectedTabIds: $selectedTabIds,
lastSidebarSelectionIndex: $lastSidebarSelectionIndex,
targetRowHeight: nil,
dragAutoScrollController: dragAutoScrollController
))
}
}
🤖 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 `@Sources/ContentView.swift` around lines 10620 - 10635, The top drop zone
overlay is gated on both dragState.draggedTabId != nil and firstWorkspaceId
existing, which prevents the SidebarTabDropDelegate from mounting during foreign
(cross-window) drags. Remove the dragState.draggedTabId != nil check from the if
condition so the overlay remains mounted whenever firstWorkspaceId exists,
allowing the delegate to properly validate the payload and handle cross-window
drags through activateForeignDragIfNeeded().

Comment thread Sources/ContentView.swift
Comment on lines 12308 to +12321
private func requestClearSoon(reason: String) {
guard pendingClearTimer == nil else { return }
guard pendingClearWorkItem == nil else { return }
#if DEBUG
cmuxDebugLog("sidebar.dragFailsafe.schedule reason=\(reason)")
#endif
let timer = DispatchSource.makeTimerSource(queue: .main)
pendingClearGeneration &+= 1
let generation = pendingClearGeneration
timer.schedule(deadline: .now() + SidebarDragFailsafePolicy.clearDelay)
timer.setEventHandler { [weak self] in
Task { @MainActor [weak self] in
guard let self, self.pendingClearGeneration == generation else { return }
let workItem = DispatchWorkItem { [weak self] in
#if DEBUG
cmuxDebugLog("sidebar.dragFailsafe.fire reason=\(reason)")
cmuxDebugLog("sidebar.dragFailsafe.fire reason=\(reason)")
#endif
self.pendingClearTimer = nil
self.onRequestClear?(reason)
}
self?.pendingClearWorkItem = nil
self?.onRequestClear?(reason)
}
pendingClearTimer = timer
timer.resume()
pendingClearWorkItem = workItem
DispatchQueue.main.asyncAfter(deadline: .now() + SidebarDragFailsafePolicy.clearDelay, execute: workItem)

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 | 🟠 Major | ⚡ Quick win

Replace the delayed failsafe with an explicit drag-state transition.

DispatchQueue.main.asyncAfter adds a timing repair path in production UI/drag state code, which can hide lifecycle races and create delayed stale-state clears. Clear from the concrete monitor event, or move the transition into the drag lifecycle owner instead of relying on a delay. As per coding guidelines, “Flag DispatchQueue.asyncAfter ... in shipped app/runtime Swift code as failures by default.”

🐛 Proposed direction
     private func requestClearSoon(reason: String) {
-        guard pendingClearWorkItem == nil else { return }
 `#if` DEBUG
-        cmuxDebugLog("sidebar.dragFailsafe.schedule reason=\(reason)")
+        cmuxDebugLog("sidebar.dragFailsafe.clear reason=\(reason)")
 `#endif`
-        let workItem = DispatchWorkItem { [weak self] in
-#if DEBUG
-            cmuxDebugLog("sidebar.dragFailsafe.fire reason=\(reason)")
-#endif
-            self?.pendingClearWorkItem = nil
-            self?.onRequestClear?(reason)
-        }
-        pendingClearWorkItem = workItem
-        DispatchQueue.main.asyncAfter(deadline: .now() + SidebarDragFailsafePolicy.clearDelay, execute: workItem)
+        onRequestClear?(reason)
     }
🤖 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 `@Sources/ContentView.swift` around lines 12308 - 12321, Remove the delayed
failsafe mechanism in the requestClearSoon function that uses
DispatchQueue.main.asyncAfter with SidebarDragFailsafePolicy.clearDelay. Instead
of scheduling a delayed clear via DispatchWorkItem, integrate the clearing logic
directly into the drag lifecycle state machine or trigger the onRequestClear
callback from a concrete monitor event that represents the actual end of the
drag operation. This eliminates the timing-based repair path and makes the state
transition explicit and predictable rather than relying on a delayed dispatch
that can hide lifecycle races.

Source: Coding guidelines

@azooz2003-bit
azooz2003-bit merged commit f24eeb8 into main Jun 23, 2026
49 checks passed
azooz2003-bit added a commit that referenced this pull request Jun 27, 2026
* Reapply "Fix workspace group drag drop intent (#6532)" (#6713)

This reverts commit f24eeb8.

* test: exercise top-level scope explicitly in group bottom-indicator assertion

Address CodeRabbit: pass indicatorScope: .topLevel so the assertion validates
the top-level branch instead of relying on the default .raw value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ci: bump WorkspaceGroupTests.swift length budget 1030->1031

The indicatorScope: .topLevel test assertion fix added one line, pushing the
file one over budget. Accept the +1 for the test-correctness improvement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — a8d37a24 Deployed Jun 23, 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.

2 participants