Repository navigation
Animate sidebar workspace reordering - #5034
lawrencecchen wants to merge 10 commits into
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds group-aware drag-preview computation and integrates it into sidebar rendering: drag state now supports group drop previews and cursor-based autoscroll, preview item lists are computed and animated, invisible drag previews are used for OS visuals, and tests validate preview behaviors. ChangesWorkspace Drag/Drop Preview and Animation
Sequence Diagram(s)sequenceDiagram
participant DragState
participant VerticalTabsSidebar
participant SidebarWorkspaceRenderItem
participant TabManager
participant SidebarEmptyArea
DragState->>VerticalTabsSidebar: provide draggedWorkspaceId, dropIndicator, reorderIds, groupDropPreview
VerticalTabsSidebar->>SidebarWorkspaceRenderItem: dragPreviewItems(items, draggedId, dropIndicator, reorderIds, groupDropPreview)
SidebarWorkspaceRenderItem-->>VerticalTabsSidebar: preview renderItems
VerticalTabsSidebar->>VerticalTabsSidebar: LazyVStack animation keyed by renderItems.ids
VerticalTabsSidebar->>SidebarEmptyArea: SidebarWorkspaceTopDropIndicator visibility update
VerticalTabsSidebar->>TabManager: performDrop / addWorkspaceToGroup
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
Caution Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional.
❌ Failed checks (4 errors, 1 warning)
✅ Passed checks (13 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR adds live drag-hover reorder previews to the sidebar workspace list, animating rows into their pending drop positions before the user releases the mouse. It introduces cursor-following drag chrome, group-aware block moves, and in-group flat reorders.
Confidence Score: 4/5Safe to merge pending resolution of the collapsed-group member leak and the sidebar-body broad invalidation flagged in prior reviews, both of which remain unaddressed in this diff. The collapsed-group groupDropPreviewItems bug (a visible member row rendered below a visually collapsed header) and the dropIndicator/groupDropPreview reads inside VerticalTabsSidebar.body (which re-evaluates the entire sidebar on every hover-target change during drag) were identified in earlier review rounds and are still present in the updated code. A new O(n) scan pair inside SidebarWorkspaceDragFollowerOverlay.body runs at cursor-drag event frequency and is the only new finding, but it does not change the merge safety picture materially. Sources/SidebarWorkspaceRenderItem.swift (groupDropPreviewItems isCollapsed guard) and Sources/ContentView.swift (dragState property reads in workspaceRows / SidebarWorkspaceDragFollowerOverlay scan) Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant NSEventMonitor as NSEvent Local Monitor
participant AutoScroll as SidebarDragAutoScrollController
participant DragState as SidebarDragState
participant SidebarBody as VerticalTabsSidebar.body
participant Follower as SidebarWorkspaceDragFollowerOverlay
participant DropDelegate as SidebarTabDropDelegate
User->>NSEventMonitor: leftMouseDragged
NSEventMonitor->>AutoScroll: "recordDragLocation() [Task @MainActor]"
AutoScroll->>DragState: setDragLocationInDocument(point)
DragState-->>Follower: invalidate (dragLocationInDocument changed)
Follower->>Follower: body — O(n) first(where:) scan for dragged item
Follower->>Follower: position overlay at (frame.midX, cursor.y)
User->>DropDelegate: dropUpdated (new hover row)
DropDelegate->>DropDelegate: updateDropIndicator()
DropDelegate->>DragState: setDropIndicator(indicator, usesTopLevelRows)
DragState-->>SidebarBody: invalidate (dropIndicator changed)
SidebarBody->>SidebarBody: dragPreviewItems() — recompute reordered list
SidebarBody->>Follower: new sourceItems / renderItems
User->>DropDelegate: performDrop
alt groupDropPreview set
DropDelegate->>DropDelegate: addWorkspaceToGroup(.end)
else normal reorder
DropDelegate->>DropDelegate: sidebarReorderWorkspaceIds(usesTopLevelRows)
DropDelegate->>DropDelegate: moveWorkspace(from:to:)
end
DropDelegate->>DragState: clearDrag()
Reviews (10): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile |
| let blockIds = blocks.map(\.workspaceId) | ||
| let reorderIds = reorderWorkspaceIds.filter { blockIds.contains($0) } |
There was a problem hiding this comment.
O(n²) array scan on main thread during drag
blockIds is a plain [UUID] array, so every call to blockIds.contains($0) in the filter closure is an O(n) linear scan, making the overall filter O(|reorderWorkspaceIds| × |blockIds|). In the flat drag scope (reorderWorkspaceIds = tabs.map(\.id)), both sides scale with the full workspace count. At ~1000 workspaces this is ~1 000 000 UUID comparisons on the @MainActor on every body refresh where a drag is active. Wrapping blockIds in a Set before the filter reduces this to O(n + m).
Rule Used: Flag production code that adds nested full-collect... (source)
| .opacity(isBeingDragged ? 0.6 : 1) | ||
| .animation(.snappy(duration: 0.24, extraBounce: 0.02), value: isBeingDragged) | ||
| .overlay { | ||
| SidebarWorkspaceRowHoverTracker(rowInteractionState: $rowInteractionState) |
There was a problem hiding this comment.
The
.snappy(duration: 0.24, extraBounce: 0.02) parameters are hardcoded inline here and in SidebarWorkspaceGroupHeaderView instead of sharing VerticalTabsSidebar.workspaceReorderAnimation. Since those row types can't access the private static let, consider moving the constant to a shared home (e.g., a SidebarWorkspaceListMetrics-style enum) so the three sites stay in sync.
| .opacity(isBeingDragged ? 0.6 : 1) | |
| .animation(.snappy(duration: 0.24, extraBounce: 0.02), value: isBeingDragged) | |
| .overlay { | |
| SidebarWorkspaceRowHoverTracker(rowInteractionState: $rowInteractionState) | |
| .opacity(isBeingDragged ? 0.6 : 1) | |
| .animation(SidebarWorkspaceListMetrics.reorderAnimation, value: isBeingDragged) | |
| .overlay { | |
| SidebarWorkspaceRowHoverTracker(rowInteractionState: $rowInteractionState) |
Rule Used: Flag SwiftUI changes that can cause stale state, b... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/SidebarWorkspaceRenderItem.swift`:
- Around line 91-92: Replace the O(n×m) lookup by building a Set of workspace
IDs and using O(1) membership checks: instead of creating blockIds as an Array
from blocks.map(\.workspaceId) and calling blockIds.contains($0) in the filter,
create a Set (e.g., blockIdSet = Set(blocks.map(\.workspaceId))) and use
blockIdSet.contains($0) when computing reorderIds (reorderWorkspaceIds.filter {
... }); update any variable names (blockIds → blockIdSet) to reflect the change.
🪄 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: b235f4cb-b273-459b-8760-c7f7721d21b8
📒 Files selected for processing (4)
Sources/ContentView.swiftSources/SidebarWorkspaceGroupHeaderView.swiftSources/SidebarWorkspaceRenderItem.swiftcmuxTests/WorkspaceGroupTests.swift
| let blockIds = blocks.map(\.workspaceId) | ||
| let reorderIds = reorderWorkspaceIds.filter { blockIds.contains($0) } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Use Set for O(1) lookup instead of Array.contains.
blockIds.contains($0) is O(m) per call, making the filter O(n×m). This runs on the drag-hover path which fires frequently during drag operations.
♻️ Proposed fix
- let blockIds = blocks.map(\.workspaceId)
- let reorderIds = reorderWorkspaceIds.filter { blockIds.contains($0) }
+ let blockIdSet = Set(blocks.map(\.workspaceId))
+ let reorderIds = reorderWorkspaceIds.filter { blockIdSet.contains($0) }As per coding guidelines: algorithmic-complexity.md flags "nested full-collection scans... in hot UI/socket/search/process paths" for "paths expected to handle roughly 1000 workspaces."
🤖 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/SidebarWorkspaceRenderItem.swift` around lines 91 - 92, Replace the
O(n×m) lookup by building a Set of workspace IDs and using O(1) membership
checks: instead of creating blockIds as an Array from blocks.map(\.workspaceId)
and calling blockIds.contains($0) in the filter, create a Set (e.g., blockIdSet
= Set(blocks.map(\.workspaceId))) and use blockIdSet.contains($0) when computing
reorderIds (reorderWorkspaceIds.filter { ... }); update any variable names
(blockIds → blockIdSet) to reflect the change.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/SidebarWorkspaceRenderItem.swift`:
- Around line 155-165: The preview logic is creating synthetic reorders and
visible child rows even when the drop would be a no-op or the group is
collapsed; update the .groupHeader and .workspace handling so you only modify
the group's member list and append a visible child row when necessary: in the
.groupHeader branch (case .groupHeader(let group, let memberWorkspaceIds)) only
compute and use nextMemberWorkspaceIds if memberWorkspaceIds does not already
contain preview.draggedWorkspaceId, and set targetHeaderIndex/result.append only
when a real change occurs; in the .workspace branch (case .workspace(let
workspace, _)) avoid calling item.withEffectiveGroupId(preview.targetGroupId) if
workspace.id already belongs to preview.targetGroupId; additionally, when
deciding to append a child row under the header, check the group's expanded
state (e.g., group.isExpanded / !group.isCollapsed) and only insert the visible
child when the target group is expanded. Ensure you reference
nextMemberWorkspaceIds, targetHeaderIndex, result.append(.groupHeader(...)), and
withEffectiveGroupId to locate the spots to change.
🪄 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: 4e436485-b465-4c36-b28c-3dd6fbd0361d
📒 Files selected for processing (4)
Sources/ContentView.swiftSources/SidebarWorkspaceGroupHeaderView.swiftSources/SidebarWorkspaceRenderItem.swiftcmuxTests/WorkspaceGroupTests.swift
| case .groupHeader(let group, let memberWorkspaceIds) | ||
| where group.id == preview.targetGroupId: | ||
| let nextMemberWorkspaceIds = memberWorkspaceIds.contains(preview.draggedWorkspaceId) | ||
| ? memberWorkspaceIds | ||
| : memberWorkspaceIds + [preview.draggedWorkspaceId] | ||
| targetHeaderIndex = result.count | ||
| result.append(.groupHeader(group, memberWorkspaceIds: nextMemberWorkspaceIds)) | ||
|
|
||
| case .workspace(let workspace, _) | ||
| where workspace.id == preview.draggedWorkspaceId: | ||
| draggedItem = item.withEffectiveGroupId(preview.targetGroupId) |
There was a problem hiding this comment.
Avoid synthetic reorder/expansion in no-op group previews.
If the dragged workspace already belongs to preview.targetGroupId, this helper still removes it and re-inserts it at the end of that group. And when the target group is collapsed, Lines 177-184 still insert a visible child row under the header. Both cases make the hover preview change layout even though the drop should be a no-op or the group should remain collapsed.
💡 Minimal fix sketch
) -> [SidebarWorkspaceRenderItem]? {
guard let preview else { return nil }
var draggedItem: SidebarWorkspaceRenderItem?
+ var draggedWorkspaceGroupId: UUID?
var targetHeaderIndex: Int?
+ var targetGroupIsCollapsed = false
var result: [SidebarWorkspaceRenderItem] = []
result.reserveCapacity(items.count)
for item in items {
switch item {
case .groupHeader(let group, let memberWorkspaceIds)
where group.id == preview.targetGroupId:
+ targetGroupIsCollapsed = group.isCollapsed
let nextMemberWorkspaceIds = memberWorkspaceIds.contains(preview.draggedWorkspaceId)
? memberWorkspaceIds
: memberWorkspaceIds + [preview.draggedWorkspaceId]
targetHeaderIndex = result.count
result.append(.groupHeader(group, memberWorkspaceIds: nextMemberWorkspaceIds))
case .workspace(let workspace, _)
where workspace.id == preview.draggedWorkspaceId:
+ draggedWorkspaceGroupId = workspace.groupId
draggedItem = item.withEffectiveGroupId(preview.targetGroupId)
case .groupHeader(let group, _)
where group.anchorWorkspaceId == preview.draggedWorkspaceId:
return nil
default:
result.append(item)
}
}
guard let draggedItem, let targetHeaderIndex else { return nil }
+ if draggedWorkspaceGroupId == preview.targetGroupId {
+ return items
+ }
+ if targetGroupIsCollapsed {
+ return result
+ }
var insertionIndex = targetHeaderIndex + 1
while insertionIndex < result.endIndex {
guard result[insertionIndex].effectiveGroupId == preview.targetGroupId else {
break
}Also applies to: 176-184
🤖 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/SidebarWorkspaceRenderItem.swift` around lines 155 - 165, The preview
logic is creating synthetic reorders and visible child rows even when the drop
would be a no-op or the group is collapsed; update the .groupHeader and
.workspace handling so you only modify the group's member list and append a
visible child row when necessary: in the .groupHeader branch (case
.groupHeader(let group, let memberWorkspaceIds)) only compute and use
nextMemberWorkspaceIds if memberWorkspaceIds does not already contain
preview.draggedWorkspaceId, and set targetHeaderIndex/result.append only when a
real change occurs; in the .workspace branch (case .workspace(let workspace, _))
avoid calling item.withEffectiveGroupId(preview.targetGroupId) if workspace.id
already belongs to preview.targetGroupId; additionally, when deciding to append
a child row under the header, check the group's expanded state (e.g.,
group.isExpanded / !group.isCollapsed) and only insert the visible child when
the target group is expanded. Ensure you reference nextMemberWorkspaceIds,
targetHeaderIndex, result.append(.groupHeader(...)), and withEffectiveGroupId to
locate the spots to change.
| var insertionIndex = targetHeaderIndex + 1 | ||
| while insertionIndex < result.endIndex { | ||
| guard result[insertionIndex].effectiveGroupId == preview.targetGroupId else { | ||
| break | ||
| } | ||
| insertionIndex += 1 | ||
| } | ||
| result.insert(draggedItem, at: insertionIndex) |
There was a problem hiding this comment.
Collapsed group leaks a visible member row in the preview
groupDropPreviewItems never inspects group.isCollapsed. When a workspace is dragged over a collapsed group's header center, setGroupDropPreview is called (no isCollapsed guard in groupHeaderCenterDropAction), and this function finds targetHeaderIndex, skips the while-loop immediately (no visible member rows exist for a collapsed group), and inserts the dragged item at targetHeaderIndex + 1. The result is a workspace row with memberIndent rendered directly below a header that visually shows no members — an indented row appears inside a group that appears collapsed. The fix is to return nil early when group.isCollapsed, matching renderItems's own skipChildrenUntilNextGroup logic.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/ContentView.swift`:
- Around line 11892-11894: The closure passed to sidebarIndexForTabId currently
calls tabManager.tabs.firstIndex which does a linear scan on every drag-hover;
change it to use the precomputed O(1) lookup renderContext.tabIndexById (already
in scope) instead of touching tabManager so hover updates don't rescans the
list. Locate the sidebarIndexForTabId closure and replace the body that calls
tabManager.tabs.firstIndex { $0.id == workspaceId } with a lookup into
renderContext.tabIndexById[workspaceId] (or equivalent API) and return that
index (or nil) so the drag overlay uses the precomputed map.
- Around line 12152-12156: The body currently does a repeated O(n) search via
renderItems.first(where:) on every dragLocationInDocument tick; instead, when
you build the preview list (where renderItems is populated) create and store a
dictionary lookup keyed by representedWorkspaceId (e.g.,
renderItemByWorkspaceId: [WorkspaceID: RenderItem]) and replace
renderItems.first(where:) in this view with a constant-time lookup
(renderItemByWorkspaceId[draggedWorkspaceId]); keep using
dragState.draggedTabId, dragState.dragLocationInDocument and
anchors[draggedWorkspaceId] as before.
🪄 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: fa5e4663-b084-4190-b058-d8a9b4248ce9
📒 Files selected for processing (3)
Sources/ContentView.swiftSources/SidebarWorkspaceGroupHeaderView.swiftSources/VerticalTabsSidebar+WorkspaceGroups.swift
| var body: some View { | ||
| if let draggedWorkspaceId = dragState.draggedTabId, | ||
| let dragLocation = dragState.dragLocationInDocument, | ||
| let item = renderItems.first(where: { $0.representedWorkspaceId == draggedWorkspaceId }), | ||
| let anchor = anchors[draggedWorkspaceId] { |
There was a problem hiding this comment.
Don’t rescan renderItems on every drag-location tick.
dragLocationInDocument changes continuously while dragging, so this body gets reevaluated at mouse-move/autoscroll cadence. renderItems.first(where:) turns that into an O(n) walk on the hot path; precompute a lookup keyed by representedWorkspaceId when the preview list is built and read that here.
♻️ Proposed fix
`@MainActor`
private struct SidebarWorkspaceDragFollowerOverlay: View {
let dragState: SidebarDragState
- let renderItems: [SidebarWorkspaceRenderItem]
+ let renderItemsByWorkspaceId: [UUID: SidebarWorkspaceRenderItem]
let anchors: [UUID: Anchor<CGRect>]
let proxy: GeometryProxy
let rowContent: (SidebarWorkspaceRenderItem) -> AnyView
var body: some View {
if let draggedWorkspaceId = dragState.draggedTabId,
let dragLocation = dragState.dragLocationInDocument,
- let item = renderItems.first(where: { $0.representedWorkspaceId == draggedWorkspaceId }),
+ let item = renderItemsByWorkspaceId[draggedWorkspaceId],
let anchor = anchors[draggedWorkspaceId] {
let frame = proxy[anchor]
rowContent(item)As per coding guidelines, **/*.{swift,ts,tsx,js,jsx,mjs,cjs,sh,zsh} applies .github/review-bot-rules/algorithmic-complexity.md and fails repeated full-collection scans in hot UI paths.
🤖 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 12152 - 12156, The body currently
does a repeated O(n) search via renderItems.first(where:) on every
dragLocationInDocument tick; instead, when you build the preview list (where
renderItems is populated) create and store a dictionary lookup keyed by
representedWorkspaceId (e.g., renderItemByWorkspaceId: [WorkspaceID:
RenderItem]) and replace renderItems.first(where:) in this view with a
constant-time lookup (renderItemByWorkspaceId[draggedWorkspaceId]); keep using
dragState.draggedTabId, dragState.dragLocationInDocument and
anchors[draggedWorkspaceId] as before.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8024587. Configure here.
| let renderItems = SidebarWorkspaceRenderItem.dragPreviewItems( | ||
| baseRenderItems, | ||
| draggedWorkspaceId: dragState.draggedTabId, | ||
| dropIndicator: dragState.dropIndicator, | ||
| reorderWorkspaceIds: renderContext.sidebarReorderIds, | ||
| groupDropPreview: dragState.groupDropPreview | ||
| ) |
There was a problem hiding this comment.
dragState.dropIndicator read in sidebar body breaks @Observable isolation invariant
workspaceRows is a plain function called directly from VerticalTabsSidebar.body, not a separate View struct. With @Observable, every property access inside a body evaluation registers on the calling view — so reading dragState.dropIndicator and dragState.groupDropPreview here makes VerticalTabsSidebar itself an observer of those properties. Every hover-target change during drag (each time updateDropIndicator fires with a new target) now re-evaluates the entire VerticalTabsSidebar.body and recomputes dragPreviewItems on the main thread.
This is exactly the broad invalidation that the SidebarDragState comment says was fixed by issue #2586: "never the sidebar body or the LazyVStack itself." The updated comment in this PR softens that claim to "without rebuilding on every drag-location tick," which is accurate for dragLocationInDocument — but dropIndicator still triggers full-body rebuilds on every hover change.
The fix is to move the dragPreviewItems computation into its own @MainActor View struct so @Observable scopes the invalidation to that sub-view and leaves VerticalTabsSidebar.body unaffected during drag.
Rule Used: Flag SwiftUI changes that can cause stale state, b... (source)

Summary
Testing
git diff --check./scripts/reload.sh --tag sidebar-animwas attempted locally, but the build was interrupted after a long Xcode LLVM codegen stall in an unrelated large Swift batch; no valid tagged build was produced.Issues
Need help on this PR? Tag
@codesmithwith what you need. Autofix is disabled.Note
Medium Risk
Large UI interaction changes in sidebar drag/drop and autoscroll with performance-sensitive @observable invalidation; mitigated by tests and guards around preview/drop state, but regressions in grouping or drop targets are possible.
Overview
Sidebar workspace drag-and-drop now previews the final list order while hovering, with snappy animations on row moves and top insertion indicators, instead of only updating on drop.
Reorder preview builds a temporary render list via
SidebarWorkspaceRenderItem.dragPreviewItems, including group-center “add to group” previews (groupDropPreview) separate from edge insertion indicators. Expanded groups can move as a single top-level block; in-group reorders stay flat; dragging a member out can show promoted placement viaeffectiveGroupId.Drag visuals hide the system drag image (
SidebarWorkspaceInvisibleDragPreview), nearly hide the source row, and show a cursor-following overlay sized from a one-time frame snapshot plus live document Y fromSidebarDragAutoScrollController(local mouse monitor + scroll tracking).Drop stability ignores self-target hovers, avoids clearing indicators when the dragged row passes under the pointer during animated preview, and can commit add-to-group from a group-center preview on drop.
Tests in
WorkspaceGroupTestscover block moves, in-group reorder, collapsed headers, promotion, and missing-indicator no-ops.Reviewed by Cursor Bugbot for commit 618ba98. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Adds live drag-hover reorder previews with snappy list animations and a cursor-following drag row that stays pinned through autoscroll. Keeps insertion indicators and group-center join previews stable so layouts don’t snap while items move under the pointer.
New Features
SidebarWorkspaceRenderItem.dragPreviewItems: expanded groups move as a block at top level, in-group reorders stay flat, collapsed headers behave as single rows, and members can be promoted out of a group; indentation tracked witheffectiveGroupId.Bug Fixes
Written for commit 618ba98. Summary will update on new commits.
Summary by CodeRabbit