Skip to content

Drag a workspace into another window's sidebar to move it - #5399

Merged
austinywang merged 9 commits into
mainfrom
issue-5395-drag-workspace-to-window
Jun 5, 2026
Merged

austinywang merged 9 commits into
mainfrom
issue-5395-drag-workspace-to-window

Conversation

@austinywang

@austinywang austinywang commented Jun 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implements #5395: drag a workspace row out of one cmux window's sidebar and drop it into another window's sidebar to move it there (same effect as the existing right-click "Move Workspace to Window"), inserted at the drop position.

Previously a sidebar workspace drag could only reorder within its own window. The drop delegate derived the dragged identity solely from per-window SidebarDragState.draggedTabId, which is only set in the window where the drag began — so a drop whose source was a different window had no identity to act on and was silently rejected.

Unified drop path (one source of truth)

Rather than fork a parallel cross-window drop delegate, the sidebar drop path now resolves the dragged workspace identity from a single source of truth:

  • SidebarDragState.draggedTabId when present (intra-window), else
  • SidebarTabDragPayload.currentDraggedWorkspaceId() — read synchronously from the drag pasteboard, authoritative in any window.

SidebarTabDropDelegate keys one branch on whether the dragged workspace is already in this window:

  • In this window → existing reorder (unchanged).
  • Another window → cross-window move at the drop index, mirroring AppDelegate.moveWorkspaceToWindow (now with an atIndex: parameter).

A foreign hover mirrors its identity into the destination window's SidebarDragState so the existing drop-indicator / frame-anchor / failsafe machinery (all gated on draggedTabId != nil) activate unchanged — the id matches no local row so nothing dims, and the mouse-up failsafe clears it. A new pure SidebarDropPlanner.crossWindowInsertion(...) computes the insertion index + indicator for the case where the dragged workspace is not in the list (no source index to remove). The group-header drop surface inherits cross-window support for free via the reorder delegate it wraps.

Edge cases handled

  • Dropping back onto the source window → plain reorder (no-op move).
  • Dropping onto the empty area of the destination → appends.
  • Multi-selected workspaces move together (resolved from the source window's sidebar selection).
  • Dragging out the last workspace of a window → detachWorkspace leaves the source with a fresh workspace (no crash / broken empty window), same as the existing move action.
  • Pinned/unpinned region is respected at the insertion point.

Tests

  • SidebarDropPlannerTests: crossWindowInsertion — top/bottom edge, empty-area append, pinned-region clamping (both directions), and drop-time indicator replay (pure, no app launch).
  • CrossWindowWorkspaceMoveTests: the detach/attach-at-index move core across two TabManagers — workspace lands at the drop index in the destination, source stays consistent, last-workspace detach keeps source non-empty.

The drag gesture / drop targeting itself is UI-only (SwiftUI onDrop across windows) and is verified by dogfood rather than unit tests.

Localization

No new user-facing strings introduced (drag/drop is visual; the only added strings are #if DEBUG diagnostics). No keyboard shortcut added (drag-driven).

🤖 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
Touches multi-window tab routing, drag lifecycle, and sidebar ordering invariants (pins/groups); mistakes could mis-route workspaces or leave stale drag state, but behavior is heavily tested and gated.

Overview
Enables moving workspaces between windows by dragging from one sidebar into another, at the drop position (same outcome as “Move Workspace to Window”), instead of only reordering within the originating window.

Drag identity and drop path: A process-wide SidebarWorkspaceDragRegistry records the dragged workspace synchronously (avoiding pasteboard races). SidebarTabDropDelegate resolves the dragged id from local state or the registry, mirrors foreign drags into destination SidebarDragState for existing indicators/failsafes, and branches to cross-window commit vs in-window reorder. Clearing drag state via bindings now calls clearDrag() so the registry cannot go stale.

Placement rules: SidebarDropPlanner.crossWindowInsertion plans insertion and indicators when the dragged workspace is not in the destination list, including pinned/unpinned clamps. AppDelegate.moveWorkspaceToWindow gains optional atIndex:; drops can move multi-selection (excluding group anchors), plan per pin tier, and focus the grabbed tab. Cross-window drops reject group anchor drags; attachWorkspace re-normalizes group contiguity after indexed inserts.

Tests: New coverage for cross-window insertion math and detach/attach move behavior (index, last tab, pinned front, group contiguity).

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


Summary by cubic

Add cross-window drag-and-drop for workspaces. Drag a workspace from one window’s sidebar into another window’s sidebar to move it to the drop position (implements #5395), while keeping pinned-first and group-contiguity rules intact; group headers cannot be moved across windows.

  • New Features

    • Drag a workspace into another window’s sidebar; honors drop index, pinned vs unpinned regions, and top-level group boundaries (hovering a group member targets its anchor).
    • Multi-select moves together and preserves source order per pin tier (base-per-tier + running offset); anchors are excluded; dropping on empty area appends; dropping in the source window reorders.
    • Unified drag identity: local SidebarDragState.draggedTabId, else process-wide SidebarWorkspaceDragRegistry.currentWorkspaceId (no pasteboard race). Destination mirrors the id and caches the foreign pin state; SidebarDropPlanner.crossWindowInsertion(...) plans in top-level space; AppDelegate.moveWorkspaceToWindow(..., atIndex:) commits at the drop position.
  • Bug Fixes

    • Reject cross-window drops when the dragged id is a source group anchor to keep the group intact; cache the foreign dragged workspace’s pin state once per drag to avoid hover-time scans.
    • Enforce destination invariants on attach: TabManager.attachWorkspace(at:) re-normalizes to preserve the leading pinned segment and contiguous group runs; clamp a pinned workspace dropped into a window with no pins to index 0 so the indicator matches final placement.
    • Clear the process-wide drag registry when extension/browser sidebars finish a drag by writing nil through the drag binding, preventing stale cross-window drag ids.

Written for commit 38f486b. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Move workspaces across windows and insert them at a specified position in the destination sidebar; cross-window drops focus moved workspaces.
  • Improvements

    • Process-wide cross-window drag identity, unified drop validation/hover indicators, and pinned-aware insertion rules that preserve pinned/unpinned and group contiguity; sidebar restores selection after moves.
  • Tests

    • Added tests for cross-window insertion indexing, pinned placement, appending behavior, selection, and group contiguity.

Today a sidebar workspace drag can only reorder within its own window;
moving to another window required the right-click "Move Workspace to
Window" menu. The drop delegate derived the dragged identity solely from
per-window SidebarDragState.draggedTabId, which is only set in the window
where the drag began — so a drop whose source was a different window had
no identity to act on and was silently rejected.

Unify the sidebar drop path on a single source of truth for the dragged
workspace identity: SidebarDragState.draggedTabId when present, else the
drag pasteboard payload (SidebarTabDragPayload.currentDraggedWorkspaceId),
which is authoritative in any window. SidebarTabDropDelegate now keys one
branch on whether the dragged workspace is already in this window:

- In this window  -> existing reorder (unchanged).
- Another window  -> cross-window move at the drop index, mirroring the
  existing "Move Workspace to Window" action.

A foreign hover mirrors its identity into the destination's drag state so
the existing drop-indicator / frame-anchor / failsafe machinery (all gated
on draggedTabId != nil) activate unchanged; the id matches no local row so
nothing dims, and the mouse-up failsafe clears it. A new pure planner
SidebarDropPlanner.crossWindowInsertion computes the insertion index and
indicator when the dragged workspace is not in the list. The group-header
drop surface inherits cross-window support for free via the reorder
delegate it wraps.

Edge cases: dropping back on the source window is a plain reorder;
dropping on the empty area appends; multi-selected workspaces move
together (resolved from the source window's selection); detaching the last
workspace leaves the source window with a fresh workspace.

Tests: SidebarDropPlanner.crossWindowInsertion (edges, append, pinned-
region clamping, indicator replay) and the detach/attach-at-index move
core across two TabManagers. The drag gesture / drop targeting itself is
UI-only and is exercised by dogfood, not unit tests.

Closes #5395

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 4, 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 5, 2026 1:22am
cmux-staging Building Building Preview, Comment Jun 5, 2026 1:22am

@coderabbitai

coderabbitai Bot commented Jun 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8703ba47-ff55-429f-b58c-1f25c7662733

📥 Commits

Reviewing files that changed from the base of the PR and between 5b6325d and 38f486b.

📒 Files selected for processing (6)
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Sidebar/SidebarDropPlanner.swift
  • Sources/TabManager.swift
  • cmuxTests/SidebarOrderingTests.swift
  • cmuxTests/TabManagerUnitTests.swift

📝 Walkthrough

Walkthrough

Adds a process-wide sidebar drag registry, computes legal cross-window insertion indices that respect pinned/unpinned partitions, forwards an optional insertion index through AppDelegate.moveWorkspaceToWindow to TabManager.attachWorkspace, mirrors foreign drags into local state for indicators, and adds planner and move tests.

Changes

Cross-window workspace drag-and-drop

Layer / File(s) Summary
Cross-window insertion planning logic and tests
Sources/Sidebar/SidebarDropPlanner.swift, cmuxTests/SidebarOrderingTests.swift
SidebarDropPlanner.crossWindowInsertion computes insertionIndex and indicator from prior indicator or target/pointer edge, then clamps into pinned/unpinned partitions via legalCrossWindowInsertionPosition. Tests validate append, top/bottom insertion, pinned/unpinned clamping, and indicator recovery.
AppDelegate indexed insertion wiring
Sources/AppDelegate.swift
moveWorkspaceToWindow gains an optional atIndex parameter and forwards it to destinationManager.attachWorkspace(..., at: atIndex, select: focus), enabling explicit insertion indices during cross-window moves.
TabManager contiguity enforcement
Sources/TabManager.swift
attachWorkspace(_:at:select:) now calls normalizeWorkspaceGroupContiguity() after insertion to restore pinned/group contiguous invariants for externally-moved workspaces.
ContentView drag registry and lifecycle
Sources/ContentView.swift
Adds SidebarWorkspaceDragRegistry, tracks originatedActiveDrag, and prevents non-origin windows from clearing the process-wide registry when they clear local drag state.
ContentView cross-window drop plumbing
Sources/ContentView.swift
Introduces effectiveDraggedTabId (local then registry), activates foreign-drag into local dragState, rejects cross-window group-anchor drops, uses SidebarDropPlanner.crossWindowInsertion for hover indicators and insertion planning, and routes cross-window drops to performCrossWindowDrop which moves workspaces at planner-computed top-level indices and syncs selection.
Cross-window workspace move unit tests
cmuxTests/TabManagerUnitTests.swift
Adds tests for detach-from-source + attach-to-destination: insert at index, append when index nil, moving last workspace replacement, pinned-clamping, and group contiguity preservation.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ContentView
  participant SidebarDropPlanner
  participant AppDelegate
  participant TabManagerSource
  participant TabManagerDest

  User->>ContentView: Drag workspace from source window
  ContentView->>ContentView: Resolve effectiveDraggedTabId (local or registry)
  ContentView->>SidebarDropPlanner: crossWindowInsertion(...)
  SidebarDropPlanner-->>ContentView: insertionIndex, indicator
  ContentView->>AppDelegate: moveWorkspaceToWindow(workspaceId, destWindowId, atIndex: insertionIndex)
  AppDelegate->>TabManagerSource: detachWorkspace(workspaceId)
  AppDelegate->>TabManagerDest: attachWorkspace(workspace, at: insertionIndex)
  TabManagerDest-->>AppDelegate: confirm attach
  AppDelegate-->>ContentView: success
  ContentView->>ContentView: update selection / dragState
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • Ari4ka

Poem

🐰 I hop across windows, light and spry,
A UUID clutched beneath my eye,
Pinned friends march front while others slide in,
Indices snug — no gap, no din,
Workspaces settle; the sidebar's grin.


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (4 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error PR introduces DispatchQueue.main.asyncAfter(0.15s) in SidebarDragFailsafePolicy without documenting why real signal unavailable, violating swift-blocking-runtime rule exceptions. Document why asyncAfter is needed instead of a real signal, or refactor drag cleanup to use notification/state transition.
Cmux Algorithmic Complexity ❌ Error O(k*n) violation: lines 17879-17889 loop movingIds with normalizeWorkspaceGroupContiguity per iteration. O(n) hot-path: updateCrossWindowDropIndicator calls O(n) helpers on every mouse-move event. Batch normalize after all moves; cache cross-window lists during drag; use set-based lookups instead of .first(where:) on scalable tabs array.
Cmux Swift File And Package Boundaries ❌ Error ContentView.swift added 291 lines exceeding the 250-line threshold for oversized files (18,875 lines). No extraction exception applies. Extract cross-window drag logic to reduce ContentView.swift by >200 lines or create new SwiftPM package for cross-window sidebar drops.
Cmux Architecture Rethink ❌ Error Multi-move uses one insertion index for mixed pin-state workspaces, relies on post-insertion normalization to fix—symptom patching with preview divergent from final placement. Reject mixed pin-state multi-moves or split pinned/unpinned moves so drop indicator matches final placement without post-insertion normalization.
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (13 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: enabling drag-and-drop of workspaces across windows 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 Production Swift changes properly isolated: SidebarDragState/SidebarWorkspaceDragRegistry @MainActor, methods already in @MainActor context, SidebarDropPlanner is pure enum. No actor isolation issues.
Cmux No Hacky Sleeps ✅ Passed PR modifies only Swift files (.swift); check scope is TypeScript/JavaScript/shell/non-Swift build scripts. Swift is explicitly excluded (covered by swift-blocking-runtime.md).
Cmux Swift Concurrency ✅ Passed SidebarWorkspaceDragRegistry is @MainActor; all new cross-window drop methods are synchronous; no DispatchQueue.global, fire-and-forget Tasks, or new Combine patterns.
Cmux Swift @Concurrent ✅ Passed All new synchronous methods properly respect actor isolation: @MainActor on AppDelegate/TabManager, DropDelegate on ContentView, pure utilities in SidebarDropPlanner. No async violations detected.
Cmux Swift Logging ✅ Passed Cross-window drag feature uses only DEBUG-guarded cmuxDebugLog calls; no unguarded print/debugPrint/dump/NSLog in production code or logs exposing secrets.
Cmux User-Facing Error Privacy ✅ Passed PR adds cross-window drag-and-drop functionality for workspaces with no new user-facing error messages, alerts, or sensitive information exposure.
Cmux Full Internationalization ✅ Passed All string literals added are debug-only logs wrapped in #if DEBUG blocks passed to cmuxDebugLog(). No user-facing UI text, localization keys, or catalog entries were added. Test files are exempt.
Cmux Swiftui State Layout ✅ Passed SidebarDragState properly uses @Observable, rows receive value snapshots not stores, no render-time state mutations, state changes only in event handlers, no new @Published/@StateObject for cmux.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only modifies cross-window workspace drag-and-drop without creating any new NSWindow, NSPanel, NSWindowController, or Window/WindowGroup instances.
Description check ✅ Passed Pull request description comprehensively covers all required sections: Summary (what/why), Testing (test coverage described), edge cases, and localization notes. Checklist and review trigger are included.
✨ 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 issue-5395-drag-workspace-to-window

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.

Comment thread Sources/ContentView.swift
@greptile-apps

greptile-apps Bot commented Jun 4, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Implements cross-window sidebar drag-and-drop: dragging a workspace from one window's sidebar into another moves it to the drop position, with multi-select, pinned/unpinned region clamping, and group-contiguity normalization all handled. The unified drop path resolves drag identity from a process-wide SidebarWorkspaceDragRegistry when the local SidebarDragState has no active drag, avoiding the pasteboard materialization race.

  • SidebarWorkspaceDragRegistry is a @MainActor process-global singleton set synchronously at drag start and cleared by the originating window's clearDrag(); destination windows mirror the foreign id into their local SidebarDragState for indicator and failsafe machinery.
  • SidebarTabDropDelegate branches on isCrossWindowDrag to dispatch to performCrossWindowDrop, which batches moves per pin tier with a running offset and calls moveWorkspaceToWindow(..., atIndex:) for each workspace.
  • attachWorkspace(at:) now calls normalizeWorkspaceGroupContiguity() after every insert, ensuring the destination's leading-pinned and contiguous-group invariants hold regardless of drop index; moveWorkspaceToWindow gains the same atIndex parameter and a same-manager short-circuit guard that makes the final focus-only call safe.

Confidence Score: 5/5

Safe to merge; well-tested cross-window drop path with no data-integrity issues identified.

The core move primitives (detach/attach, normalizeWorkspaceGroupContiguity, atIndex propagation) are all covered by unit tests and the same-manager short-circuit is verified in the source. The two open items are non-blocking quality improvements that don't affect workspace data integrity.

Sources/ContentView.swift — the validateDrop anchor check and dropExited cleanup are the two spots worth a follow-up pass.

Important Files Changed

Filename Overview
Sources/ContentView.swift Core of the feature: adds SidebarWorkspaceDragRegistry, cross-window drag mirroring, performCrossWindowDrop with per-pin-tier batch logic, and cross-window indicator planning; two non-blocking issues found (per-row anchor scan in validateDrop hot path, stale indicator on dropExited)
Sources/AppDelegate.swift Adds optional atIndex parameter to moveWorkspaceToWindow; same-manager guard correctly short-circuits to focus-only, so the second focus call in performCrossWindowDrop is safe
Sources/Sidebar/SidebarDropPlanner.swift New crossWindowInsertion pure function with legalCrossWindowInsertionPosition clamping; well-tested and correct
Sources/TabManager.swift attachWorkspace now calls normalizeWorkspaceGroupContiguity unconditionally after insert; the normalization is idempotent and safe for all existing call sites
cmuxTests/SidebarOrderingTests.swift Adds 7 crossWindowInsertion unit tests covering top/bottom edge, empty-area append, pinned clamping in both directions, and indicator replay
cmuxTests/TabManagerUnitTests.swift Adds CrossWindowWorkspaceMoveTests covering detach/attach at index, append, last-workspace safety, pinned-landing invariant, and group contiguity after insert

Sequence Diagram

sequenceDiagram
    participant A as Source Window (SidebarDragState A)
    participant Reg as SidebarWorkspaceDragRegistry
    participant B as Destination Window (SidebarDragState B)
    participant AD as AppDelegate
    participant TM as TabManager (dest)

    A->>Reg: begin(workspaceId:) [drag start]
    Note over A,Reg: originatedActiveDrag = true

    B->>Reg: currentWorkspaceId (validateDrop / dropEntered)
    B->>B: activateForeignDragIfNeeded()
    Note over B: draggedTabId = foreignId, foreignDraggedIsPinned cached

    B->>B: updateCrossWindowDropIndicator() [dropUpdated]

    B->>AD: performCrossWindowDrop(draggedTabId:)
    AD->>AD: moveWorkspaceToWindow(atIndex:) per tier
    AD->>TM: detachWorkspace(source) + attachWorkspace(at:)
    TM->>TM: normalizeWorkspaceGroupContiguity()
    AD->>AD: moveWorkspaceToWindow(focus:true) — same-manager → focus only

    A->>Reg: end(workspaceId:) [clearDrag on mouse-up]
    B->>B: clearDrag() [failsafe mouse-up]
    Note over B: draggedTabId = nil, foreignDraggedIsPinned = nil
Loading

Reviews (8): Last reviewed commit: "Preserve source order in cross-window mu..." | Re-trigger Greptile

Comment thread Sources/ContentView.swift Outdated
Comment on lines +17757 to +17768
var didMove = false
for (offset, workspaceId) in movingIds.enumerated() {
let isLast = offset == movingIds.count - 1
if app.moveWorkspaceToWindow(
workspaceId: workspaceId,
windowId: destinationWindowId,
atIndex: insertionIndex + offset,
focus: isLast
) {
didMove = true
}
}

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 Multi-select offset doesn't account for failed moves

atIndex: insertionIndex + offset uses the enumeration index, not a counter of successfully-inserted workspaces. If any moveWorkspaceToWindow call in the batch returns false (e.g., workspace disappeared between list construction and the loop), the next workspace attempts insertion at insertionIndex + (N+1) when the destination has only grown by N-1, leaving a gap. The same stale IDs also end up in selectedTabIds regardless of which moves succeeded. While the failure path is unreachable under normal @mainactor execution, tracking successful-move count is more defensive and matches the invariant that each successful insertion shifts all later target indices by 1.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 1225cf0 — the loop now offsets each insert by the count of successful moves (atIndex: rawInsertIndex + movedIds.count) and only the workspaces that actually moved become the destination selection, so a skipped move can't leave an index gap or stale selection.

— Claude Code

Comment thread Sources/ContentView.swift Outdated
Comment on lines +17815 to +17818
private func updateCrossWindowDropIndicator(for info: DropInfo, draggedTabId: UUID) {
let draggedIsPinned = AppDelegate.shared?
.tabManagerFor(tabId: draggedTabId)?
.tabs.first { $0.id == draggedTabId }?.isPinned ?? false

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 Repeated tabManagerFor lookup on every pointer-move event

draggedIsPinned is re-resolved from AppDelegate.shared?.tabManagerFor(tabId:)?.tabs.first { ... } on each call to updateCrossWindowDropIndicator, which fires for every dropUpdated (every pointer-move during the hover). tabManagerFor scans all open windows' tab managers — O(windows × workspaces) per event. The isPinned status of the dragged workspace cannot change mid-drag, so the result could be resolved once (e.g., in activateForeignDragIfNeeded and stashed, or cached as a local in dropUpdated) rather than re-fetched on each pointer-move event.

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!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 1225cf0 — the foreign workspace's pin state is now resolved once when the drag is mirrored into the destination (stashed on SidebarDragState.foreignDraggedIsPinned) and reused for every hover, so there's no per-pointer-move tabManagerFor scan across windows.

— Claude Code

Address codex review: the cross-window drop inserted a foreign workspace
at a raw tabs index, which could split a destination group's contiguous
run or drop a pinned workspace below unpinned ones.

Fix at the ownership boundary: TabManager.attachWorkspace now re-runs
normalizeWorkspaceGroupContiguity() after insert — the same normalization
addWorkspace uses — so every cross-window attach (this drag path and the
CLI move-workspace-to-window path, which shared the latent bug) keeps the
leading-pinned segment and contiguous group runs intact regardless of the
requested drop index. This also makes per-pin-tier correctness for a
mixed multi-selection fall out for free, since each attach normalizes by
the moved workspace's own pin state.

Also clamp the cross-window drop indicator for a pinned workspace dragged
into a window with no existing pins to the front (index 0) instead of the
raw pointer position, so the indicator matches where the workspace lands.

Tests: pinned-into-no-pins planner clamp; pinned workspace lands at front
even when dropped below unpinned rows; moving into the middle of a group
run keeps the group contiguous.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread Sources/ContentView.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 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 17815-17827: The updateCrossWindowDropIndicator hot path rebuilds
tabManager.tabs snapshots every hover; fix by capturing a single snapshot of tab
IDs and pinned IDs at drag start and reusing it in
updateCrossWindowDropIndicator (and the eventual commit path) instead of calling
tabManager.tabs.map/filter each event. Add cached properties (e.g.,
dragSessionTabIds: [UUID] and dragSessionPinnedTabIds: Set<UUID>) that are
populated when the drag begins and cleared when the drag ends, then pass those
cached values into SidebarDropPlanner.crossWindowInsertion (replace
tabManager.tabs.map(\\.id) and
Set(tabManager.tabs.filter(\\.isPinned).map(\\.id)) with the cached variables).
Ensure the cache is updated/invalidated in the drag start/finish handlers so
other flows still see fresh data.
- Around line 17722-17775: performCrossWindowDrop currently always calls
AppDelegate.moveWorkspaceToWindow(...atIndex:) which flattens cross-window drops
and loses group/section membership; change it to resolve the destination
section/header for the foreign drop (using the same logic/path used by
SidebarTabDropDelegate.performDrop(info:)) and funnel moves through the
section-aware APIs (e.g. moveWorkspaceToSection(...) or
removeWorkspaceFromSection(...) when appropriate) instead of always using
moveWorkspaceToWindow. Ensure you compute the correct target section and
insertion index within that section (preserve draggedIsPinned and focus behavior
for the last moved item) and update selectedTabIds/syncSidebarSelection exactly
as before when didMove is true.
🪄 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: 78e6641d-e43f-472d-af6a-5dec30ea8c76

📥 Commits

Reviewing files that changed from the base of the PR and between 5b6325d and aee8dda.

📒 Files selected for processing (5)
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Sidebar/SidebarDropPlanner.swift
  • cmuxTests/SidebarOrderingTests.swift
  • cmuxTests/TabManagerUnitTests.swift

Comment thread Sources/ContentView.swift
Comment thread Sources/ContentView.swift Outdated
Address codex review (groups):

- A group-header drag carries the group's anchor id. Moving only the
  anchor across windows would dissolve the source group and strand its
  members. Reject cross-window drops whose dragged workspace is a source
  group anchor (and drop any anchors from a multi-selection), so the group
  stays intact in its window — validateDrop returns false, no destination
  indicator is shown, and the drag snaps back. Members can still be dragged
  out individually. Full cross-window group migration is out of scope.

- The destination drop planner and hover indicator now reason in the
  destination's top-level id space (group anchors represent their groups;
  a hovered group member maps to its anchor), matching the group/pin
  boundaries `attachWorkspace` normalizes incoming workspaces to. The
  top-level insertion slot is translated back to a raw `tabs` index for the
  attach, so the indicator the user sees matches where the workspace lands
  instead of pointing inside a protected group region.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@Sources/ContentView.swift`:
- Around line 17806-17814: Replace the compactMap use that returns nil when a
tab isn't selected with an idiomatic filter + map pipeline: instead of
sourceManager.tabs.compactMap { sourceSelection.contains($0.id) ? $0.id : nil }
call sourceManager.tabs.filter { sourceSelection.contains($0.id) }.map { $0.id }
so the intent for candidateIds (when computing selected tab IDs) is clearer;
update the branch that assigns candidateIds (and keep the rest using
sourceAnchorIds and movingIds unchanged).
🪄 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: df435e45-9b41-4f41-87f7-a039056f379f

📥 Commits

Reviewing files that changed from the base of the PR and between 1fb1f6c and 49b4a8d.

📒 Files selected for processing (1)
  • Sources/ContentView.swift

Comment thread Sources/ContentView.swift

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 6 files

You’re at about 98% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Re-trigger cubic

austinywang and others added 2 commits June 4, 2026 16:52
…eboard

Address codex review: the cross-window path resolved the dragged workspace
id by reading NSPasteboard(name: .drag) synchronously. SwiftUI's .onDrag
registers the payload through an NSItemProvider whose data representation
is delivered asynchronously (a Task), so a synchronous read inside a
DropDelegate can race and return nil — which would make validateDrop reject
a valid cross-window drop even though the drag type is present.

Replace the pasteboard read with SidebarWorkspaceDragRegistry: a process-
wide, main-actor-isolated record of the one workspace being sidebar-dragged.
The originating window sets it synchronously at drag start (beginDragging)
and clears it when its own drag ends; only the originating SidebarDragState
owns the entry (a destination window mirroring a foreign id for rendering
does not clear it). A destination drop delegate reads currentWorkspaceId to
resolve the dragged workspace with no materialization race. Drop-type
detection still uses the reliably-advertised UTType via hasItemsConforming.

The unused SidebarTabDragPayload pasteboard reader is removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Multi-move now offsets each insert by the count of *successful* moves
  (not the enumeration index) and selects only the workspaces that actually
  moved, so a skipped move can't leave an index gap or stale selection
  (Greptile).
- Resolve the foreign dragged workspace's pin state once when the drag is
  mirrored into the destination (stashed on SidebarDragState) instead of
  scanning every window's tab manager on each pointer-move hover (Greptile).
- Use filter + map instead of compactMap-with-nil for the selected ids
  (CodeRabbit nit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@Sources/ContentView.swift`:
- Around line 17874-17885: The multi-select move uses a single rawInsertIndex
and draggedIsPinned for all movingIds (see movingIds loop and
app.moveWorkspaceToWindow) which breaks when selection mixes pinned and unpinned
items because attachWorkspace(_:at:) re-normalizes placement; update the logic
that plans and performs the move (the planning at the draggedIsPinned/insert
calculation and the loop that calls app.moveWorkspaceToWindow) to detect mixed
pin-state and either reject/disable cross-window multi-moves in that case or
split movingIds into pinned and unpinned groups and compute/apply separate
insert indices for each group so each subgroup is inserted relative to its
pinned section (ensuring the preview indicator and final placement match).
Ensure you reference draggedIsPinned, rawInsertIndex, movingIds,
attachWorkspace(_:at:), and app.moveWorkspaceToWindow when making the change so
both planning (17935-17941) and execution (loop 17874-17885) are updated.
🪄 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: 62b75aaa-8fe7-4497-9c7e-75cc3eb0d63a

📥 Commits

Reviewing files that changed from the base of the PR and between c1a9ed9 and 1225cf0.

📒 Files selected for processing (1)
  • Sources/ContentView.swift

Comment thread Sources/ContentView.swift Outdated

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 1 file (changes from recent commits).

You’re at about 99% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread Sources/ContentView.swift Outdated
Address review (cubic + CodeRabbit P2): a cross-window multi-selection that
spans pinned and unpinned workspaces was planned from the drag initiator's
single pin bit and applied to every workspace, so attachWorkspace's per-insert
pinned/unpinned normalization could split the moved set away from the drop
indicator.

Plan each moved workspace by its own pin state against the live destination:
the hover indicator still anchors the slot, the per-iteration recompute keeps
each pin tier a contiguous block in source order (and is inherently safe
against a skipped move — a failed insert simply doesn't grow the destination,
so no index gap or stale selection), and the last successfully-moved workspace
is focused via the same-manager focus path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@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 1 potential issue.

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 c8d9de5. Configure here.

Comment thread Sources/ContentView.swift

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 1 file (changes from recent commits).

You’re at about 99% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread Sources/ContentView.swift Outdated
Address codex review: the extension / browser-stack sidebar drop delegates
end a drag by writing nil through draggedTabIdBinding, which set
dragState.draggedTabId directly and bypassed clearDrag(). Since beginDragging
now records the drag in the process-wide SidebarWorkspaceDragRegistry, those
drops left currentWorkspaceId stale, so a later cross-window drop in another
window could resolve and move the wrong workspace.

Route the binding's nil-clear through clearDrag() so any locally originated
drag also ends its registry entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…set)

Address cubic review: recomputing the insertion index per workspace from the
same fixed indicator re-anchored every insert to the hovered row, so for a
"drop after row X" (bottom edge) the batch landed in reverse order. The prior
single-base `+ movedIds.count` offset preserved order but planned the whole
batch from one pin bit (mixed-tier scatter).

Combine both: plan one base slot per pin tier (recomputed against the live
destination so a later tier sees the earlier tier's inserts), then insert that
tier's workspaces at base + running-offset. Same-tier workspaces stay a
contiguous block in source order, mixed pinned/unpinned selections each land in
their own region, and a skipped move doesn't advance the offset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@austinywang

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

All previously-flagged findings have been addressed on the current HEAD (38f486b) and replied to inline: pinned/group ordering invariants, group-anchor handling, group-aware top-level indices, the NSPasteboard→registry change, the extension-sidebar registry-leak, mixed pin-tier multi-select, and the multi-move source-order fix. CI is green. Please re-review the current commit and clear the stale changes-requested if it's resolved.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the PR details and latest diff
gh pr view 5399 --repo manaflow-ai/cmux --json state,headRefName,headRefOid,baseRefName

echo "---"
# Get current files changed
gh pr diff 5399 --repo manaflow-ai/cmux 2>/dev/null | head -3000

Length of output: 38749


🏁 Script executed:

#!/bin/bash
# Fetch the full moveWorkspaceToWindow function from AppDelegate
ast-grep --pattern 'func moveWorkspaceToWindow(workspaceId: UUID, windowId: UUID, $$$) -> Bool {
  $$$
}'

Length of output: 425


🏁 Script executed:

#!/bin/bash
# Find the full moveWorkspaceToWindow function body
rg -n "moveWorkspaceToWindow" Sources/AppDelegate.swift -A 40 | head -120

Length of output: 3602



@austinywang the re-review is complete. All seven previously-flagged findings are cleanly addressed in HEAD 38f486b47. Walkthrough:

Finding Resolution
Pinned/group ordering invariants attachWorkspace now calls normalizeWorkspaceGroupContiguity() after every insert, holding leading-pinned + contiguous-group invariants regardless of drop index.
Group-anchor cross-window drops rejected isCrossWindowGroupAnchorDrag gates both validateDrop and performCrossWindowDrop; anchor drag is rejected without stranding group members.
Group-aware top-level indices crossWindowTopLevelTabIds() passes usesTopLevelRows: true; crossWindowTopLevelTarget() resolves group members to their anchor so the planner and insert land at the group boundary.
NSPasteboard → registry SidebarWorkspaceDragRegistry is a @MainActor in-process enum set synchronously at drag start — no async materialization race.
Extension-sidebar registry leak draggedTabIdBinding nil setter now routes through clearDrag(), so browser-stack / extension drop delegates that end a drag via the binding correctly flush the registry entry.
Mixed pin-tier multi-select The for isPinnedTier in [false, true] loop plans a separate base slot per tier (recomputed against the live destination), and tierOffset advances only on success — no index gaps, no scatter.
Multi-move source-order tierIds preserves sourceManager.tabs order; offset-per-success keeps the inserted block contiguous.

One concern I had — calling moveWorkspaceToWindow(workspaceId: focusId, focus: true) after the workspace was already moved to the destination — resolves cleanly: the same-manager guard (lines 4490–4496) fires, executing focusTab + focusMainWindow + setActiveTabManager without any detach/re-attach. This is a correct and intentional reuse of that branch as a focus-and-activate shortcut.

Tests cover all the critical invariants (pin clamping, group contiguity, last-workspace detach, indicator replay). CI green. Approving.

[approve]


🧠 Learnings used
Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 4089
File: Sources/WorkspaceContentView.swift:838-1708
Timestamp: 2026-05-27T18:01:17.320Z
Learning: In manaflow-ai/cmux, extracting dock-related subviews (WorkspaceMultiDockLayoutView, WorkspaceDockToggleCluster, WorkspaceDockToggleIcon, WorkspaceDockPaneView, etc.) from Sources/WorkspaceContentView.swift into dedicated source files is explicitly deferred as a non-blocking follow-up. Do not raise this as a blocking review comment on PRs that add dock views to WorkspaceContentView.swift; the snapshot/actions decomposition (WorkspaceDockPaneSnapshot, WorkspaceDockPaneActions) already addresses the correctness risk for SwiftUI invalidation, and file splitting has no runtime impact.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 4825
File: Sources/AppDelegate.swift:921-948
Timestamp: 2026-05-26T21:44:44.637Z
Learning: In manaflow-ai/cmux PRs that make targeted fixes to AppDelegate-owned session restore, main-window registry, autosave, or display/window geometry behavior, do not insist on extracting new helper/cache logic from Sources/AppDelegate.swift when doing so would broaden the PR into an ownership refactor. It is acceptable to keep narrowly scoped geometry-cache changes in AppDelegate for a targeted bug fix if the implementation tightens boundaries with typed transition sources and guarded update paths; suggest extraction only as an optional follow-up.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 3568
File: GhosttyTabs.xcodeproj/project.pbxproj:1588-1590
Timestamp: 2026-05-05T20:41:12.478Z
Learning: In the manaflow-ai/cmux repository, the SwiftPM package-boundary extraction of `JSONCValueEditor.swift` and `CmuxSettingsManagedValues.swift` into a dedicated Foundation-only package is intentionally deferred to a separate refactor PR. For the regression/fix PR (issue `#3551`, PR `#3568`), these files are scoped to the app target and covered by behavioral CI tests. Do not flag this as an unresolved issue within the context of that PR.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 3988
File: Sources/ContentView.swift:0-0
Timestamp: 2026-05-24T03:40:53.759Z
Learning: In manaflow-ai/cmux (Swift), compute and persist sidebar selection anchors against the rendered order, not raw visible IDs. Specifically, inside Sources/ContentView.swift’s VerticalTabsSidebar, derive lastSidebarSelectionIndex from currentRenderedWorkspaceIdsForSidebarSelection() (which respects collapsedGroups) via syncLastSidebarSelectionIndexForCurrentRenderedOrder(_:). Do not use SidebarWorkspaceGroupingPlanner.plan(...).visibleWorkspaceIds outside this context, and clear the index when the selected workspace isn’t rendered.

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2646
File: Sources/ContentView.swift:0-0
Timestamp: 2026-04-14T19:57:22.309Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift — SidebarTabDropDelegate.performDrop(info:) funnels all sidebar drag-and-drop through a section-aware path: (1) if the drop target is inside a section, compute the insertion index from the destination section’s workspaceIds after removing the dragged id, then call TabManager.moveWorkspaceToSection(tabId:sectionId:atIndex:); (2) if the target is ungrouped, first remove the workspace from its source section via TabManager.removeWorkspaceFromSection(tabId:) and then call TabManager.reorderWorkspace(tabId:toIndex:). Same-index same-section drops intentionally no-op. This ensures cross-section moves update membership and same-section downward moves don’t overshoot.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3430
File: Sources/ContentView.swift:0-0
Timestamp: 2026-05-02T09:40:50.797Z
Learning: Repo: manaflow-ai/cmux — In Sidebar drop planning, below-last-row pointer positions must create a new workspace at the end. Specifically, SidebarDropPlanner.workspaceAction(for:targets:) should map points below every WorkspaceDropTarget.frame to .newWorkspace(insertionIndex: orderedTargets.count, …). This behavior is covered by unit test cmuxTests/SidebarWorkspaceDropPlannerTests.swift: testWorkspaceDropAfterLastRowCreatesWorkspaceAtEnd.

Learnt from: Znboston
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-26T19:00:12.434Z
Learning: Repo: manaflow-ai/cmux — Sources/TabManager.swift — In `moveWorkspaceOutOfGroup(_:toSidebarIndex:)`, do NOT call `syncUngroupedSidebarOrder()` after inserting into `sidebarOrder`. That helper derives order from the `tabs` array insertion index, which is unrelated to the user's drag-drop target position. `sidebarOrder` is the source of truth for sidebar rendering; the raw insertion at the resolved sidebar index is correct and sufficient. Calling `syncUngroupedSidebarOrder()` would overwrite the intended drop position with stale `tabs` order, causing a snap-back regression.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 5399
File: Sources/ContentView.swift:0-0
Timestamp: 2026-06-05T00:02:37.625Z
Learning: In `Sources/ContentView.swift` (Swift, cmux/cmux), `updateCrossWindowDropIndicator` intentionally recomputes `SidebarDropPlanner.crossWindowInsertion` on every `dropUpdated` call. This mirrors the existing local `updateDropIndicator` which already recomputes `sidebarReorderWorkspaceIds`/`sidebarReorderPinnedWorkspaceIds` the same way. `dropUpdated` is a drag-hover path (not a typing-latency hot path), sidebar workspace counts are small and bounded, and adding a cross-drag-session cache would introduce invalidation state for negligible benefit. The previously expensive per-hover `tabManagerFor` scan has been resolved by stashing the dragged workspace's pin state once at drag start rather than re-querying it on each hover event. Do not flag this recomputation as an unnecessary repeated scan.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3430
File: Sources/Sidebar/SidebarDropPlanner.swift:0-0
Timestamp: 2026-05-02T09:38:48.117Z
Learning: In manaflow-ai/cmux, `SidebarDropPlanner.workspaceAction(for:targets:)` treats a pointer position below all measured workspace rows as a valid "insert at end" drop: it computes `insertionIndex = legalNewWorkspaceInsertionIndex(orderedTargets.count, orderedTargets: orderedTargets)` and returns `.newWorkspace(insertionIndex:indicator:)`. Do NOT flag this path as missing — it is intentional and is covered by the updated `testWorkspaceDropAfterLastRowFallsThroughToEmptyAreaDropTarget` (or equivalent) test in `cmuxTests/SidebarWorkspaceDropPlannerTests.swift`.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3430
File: Sources/Sidebar/SidebarDropPlanner.swift:0-0
Timestamp: 2026-05-02T09:38:56.382Z
Learning: In manaflow-ai/cmux, `SidebarDropPlanner.workspaceAction(for:targets:)` maps pointer positions below all measured workspace rows to a `.newWorkspace` insertion at `orderedTargets.count` (end of list). It does NOT return `nil` for below-last-row drops. The behavior is covered by `testWorkspaceDropAfterLastRowCreatesWorkspaceAtEnd` in `cmuxTests/SidebarWorkspaceDropPlannerTests.swift`. (This supersedes any prior learning stating that nil was intentional for after-last-row drops.)

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3381
File: Sources/AppDelegate+MoveTabToNewWorkspace.swift:131-138
Timestamp: 2026-05-01T00:31:34.214Z
Learning: Repo: manaflow-ai/cmux — In Sources/AppDelegate+MoveTabToNewWorkspace.swift, `focusIntentForNewWorkspaceMove(panel:)` intentionally forces `.browser(.addressBar)` for `BrowserPanel` instances, overriding `preferredFocusIntentForActivation()`. This is by design: when a browser bonsplit tab is dragged into a standalone workspace, the omnibar must be surfaced regardless of the prior in-panel focus context. Do not flag this as a focus-stealing violation; the drag-to-new-workspace action is an explicit focus-intent command scoped to that path.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3247
File: Sources/ContentView.swift:0-0
Timestamp: 2026-04-29T01:08:44.497Z
Learning: Repo: manaflow-ai/cmux — Behavior contract: Sidebar “Copy Workspace ID(s)” (e.g., in Sources/ContentView.swift TabItemView context menu) must copy plain UUIDs (IDs-only), not refs. Command palette identifier-copy commands should explicitly pass includeRefs: true when refs are desired. This split preserves backward compatibility for scripts while enabling richer payloads in the palette.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 4265
File: Sources/TabManager.swift:3995-4055
Timestamp: 2026-05-18T10:44:27.336Z
Learning: In manaflow-ai/cmux, TabManager.reorderVisibleWorkspace(...) must preserve hidden-slot positions in the full tabs array. Implement by rebuilding tabs only across indices where workspace.isHidden == false (mapping reorderedVisibleIds onto those slots), and enforce pinned-at-top among visibles with clampedVisibleReorderIndex. Avoid using reorderWorkspace(before:/after:) directly for visible-only moves, since it can indirectly move interleaved hidden workspaces.

@austinywang
austinywang merged commit 087309a into main Jun 5, 2026
25 checks passed
@austinywang

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

Note: your last review only covered up to commit 1225cf0. The single actionable finding there (mixed pin-state multi-select at the cross-window move, ContentView.swift ~17874-17885) was fixed in commit 38f486b — the move now plans one base slot per pin tier and inserts each tier at base + running offset, so mixed pinned/unpinned selections land in their correct regions and same-tier order is preserved. Please re-review HEAD (38f486b); the prior changes-requested is stale.

@coderabbitai

coderabbitai Bot commented Jun 5, 2026 •

Copy link
Copy Markdown

@austinywang, acknowledged — triggering a full re-review of HEAD 38f486b47. The per-tier base-slot + running-offset fix for mixed pin-state multi-select is noted as addressed. The full review will run against the current HEAD and clear any stale findings.

Re-review request from @austinywang: the prior changes-requested (mixed pin-state multi-select scatter in ContentView.swift ~17874-17885) was fixed in commit 38f486b47 — the move now plans one base slot per pin tier (for isPinnedTier in [false, true]) and advances tierOffset only on successful inserts. Please evaluate HEAD as a clean slate and clear any stale blocking comments from prior commits.


🧠 Learnings used
Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 4089
File: Sources/WorkspaceContentView.swift:838-1708
Timestamp: 2026-05-27T18:01:17.320Z
Learning: In manaflow-ai/cmux, extracting dock-related subviews (WorkspaceMultiDockLayoutView, WorkspaceDockToggleCluster, WorkspaceDockToggleIcon, WorkspaceDockPaneView, etc.) from Sources/WorkspaceContentView.swift into dedicated source files is explicitly deferred as a non-blocking follow-up. Do not raise this as a blocking review comment on PRs that add dock views to WorkspaceContentView.swift; the snapshot/actions decomposition (WorkspaceDockPaneSnapshot, WorkspaceDockPaneActions) already addresses the correctness risk for SwiftUI invalidation, and file splitting has no runtime impact.

Learnt from: tayl0r
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-04-08T03:36:30.160Z
Learning: Repo: manaflow-ai/cmux — AppDelegate.FileBrowserDrawerState threading pattern (PR `#1909`, commit e0e57809): FileBrowserDrawerState must be threaded through AppDelegate.configure() as a weak stored property (matching the sidebarState pattern), passed through both configure() call sites, with registerMainWindow parameter made non-optional. The fallback `?? FileBrowserDrawerState()` must NOT be used as it creates detached instances that are not properly owned by the window context.

Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-06-04T02:53:43.837Z
Learning: Applies to **/*.swift : For Swift changes that add or materially change standalone cmux-owned windows, fail when the diff violates `.github/review-bot-rules/swift-auxiliary-window-close-shortcuts.md`: user-visible NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup code without a stable cmux.* identifier and shared close-shortcut ownership through cmuxAuxiliaryWindowIdentifiers

Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-06-02T22:01:27.544Z
Learning: Applies to **/*.swift : Fail when diffs to Swift architecture changes violate `.github/review-bot-rules/swift-architectural-rethink.md`: symptom patches using sleeps, delayed dispatch, polling, locks, observers, side channels, duplicate entrypoint wiring, or split UI lifecycle ownership that leaves bad state representable

Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-06-04T02:53:43.837Z
Learning: Applies to **/*.swift : For SwiftUI changes, fail when the diff violates `.github/review-bot-rules/swiftui-state-layout.md`: new ObservableObject or Published state where Observable is the modern shape, GeometryReader measurement that changes layout, lazy/list row subtrees holding store references, or render-time state mutation

Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-06-04T02:53:43.837Z
Learning: Applies to **/*.swift : For Swift architecture changes, fail when the diff violates `.github/review-bot-rules/swift-architectural-rethink.md`: symptom patches using sleeps, delayed dispatch, polling, locks, observers, side channels, duplicate entrypoint wiring, or split UI lifecycle ownership that leaves bad state representable

Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-06-02T23:06:51.822Z
Learning: Applies to **/*.swift : Fail when diffs to SwiftUI changes violate `.github/review-bot-rules/swiftui-state-layout.md`: new ObservableObject or Published state where Observable is the modern shape, GeometryReader measurement that changes layout, lazy/list row subtrees holding store references, or render-time state mutation

Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-06-02T22:01:27.544Z
Learning: Applies to **/*.swift : Fail when diffs to Swift changes that add or materially change standalone cmux-owned windows violate `.github/review-bot-rules/swift-auxiliary-window-close-shortcuts.md`: user-visible NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup code without a stable cmux.* identifier and shared close-shortcut ownership through cmuxAuxiliaryWindowIdentifiers

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 4825
File: Sources/AppDelegate.swift:921-948
Timestamp: 2026-05-26T21:44:44.637Z
Learning: In manaflow-ai/cmux PRs that make targeted fixes to AppDelegate-owned session restore, main-window registry, autosave, or display/window geometry behavior, do not insist on extracting new helper/cache logic from Sources/AppDelegate.swift when doing so would broaden the PR into an ownership refactor. It is acceptable to keep narrowly scoped geometry-cache changes in AppDelegate for a targeted bug fix if the implementation tightens boundaries with typed transition sources and guarded update paths; suggest extraction only as an optional follow-up.

Learnt from: CR
Repo: manaflow-ai/cmux PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-06-04T02:53:43.837Z
Learning: Applies to **/*.swift : For production Swift, fail when the diff violates `.github/review-bot-rules/algorithmic-complexity.md` for nested full-collection scans, per-target rescans for batch actions, repeated sorting/filtering in hot UI/socket/search/process paths, in-memory joins that belong in the data store, or unbenchmarked slower algorithms on paths expected to handle about 1000 workspaces or similar user-owned records

Learnt from: rodchristiansen
Repo: manaflow-ai/cmux PR: 2646
File: Sources/ContentView.swift:0-0
Timestamp: 2026-04-14T19:57:22.309Z
Learning: Repo: manaflow-ai/cmux — Sources/ContentView.swift — SidebarTabDropDelegate.performDrop(info:) funnels all sidebar drag-and-drop through a section-aware path: (1) if the drop target is inside a section, compute the insertion index from the destination section’s workspaceIds after removing the dragged id, then call TabManager.moveWorkspaceToSection(tabId:sectionId:atIndex:); (2) if the target is ungrouped, first remove the workspace from its source section via TabManager.removeWorkspaceFromSection(tabId:) and then call TabManager.reorderWorkspace(tabId:toIndex:). Same-index same-section drops intentionally no-op. This ensures cross-section moves update membership and same-section downward moves don’t overshoot.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3430
File: Sources/ContentView.swift:0-0
Timestamp: 2026-05-02T09:40:50.797Z
Learning: Repo: manaflow-ai/cmux — In Sidebar drop planning, below-last-row pointer positions must create a new workspace at the end. Specifically, SidebarDropPlanner.workspaceAction(for:targets:) should map points below every WorkspaceDropTarget.frame to .newWorkspace(insertionIndex: orderedTargets.count, …). This behavior is covered by unit test cmuxTests/SidebarWorkspaceDropPlannerTests.swift: testWorkspaceDropAfterLastRowCreatesWorkspaceAtEnd.

Learnt from: Znboston
Repo: manaflow-ai/cmux PR: 0
File: :0-0
Timestamp: 2026-03-26T19:00:12.434Z
Learning: Repo: manaflow-ai/cmux — Sources/TabManager.swift — In `moveWorkspaceOutOfGroup(_:toSidebarIndex:)`, do NOT call `syncUngroupedSidebarOrder()` after inserting into `sidebarOrder`. That helper derives order from the `tabs` array insertion index, which is unrelated to the user's drag-drop target position. `sidebarOrder` is the source of truth for sidebar rendering; the raw insertion at the resolved sidebar index is correct and sufficient. Calling `syncUngroupedSidebarOrder()` would overwrite the intended drop position with stale `tabs` order, causing a snap-back regression.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 5399
File: Sources/ContentView.swift:0-0
Timestamp: 2026-06-05T00:02:37.625Z
Learning: In `Sources/ContentView.swift` (Swift, cmux/cmux), `updateCrossWindowDropIndicator` intentionally recomputes `SidebarDropPlanner.crossWindowInsertion` on every `dropUpdated` call. This mirrors the existing local `updateDropIndicator` which already recomputes `sidebarReorderWorkspaceIds`/`sidebarReorderPinnedWorkspaceIds` the same way. `dropUpdated` is a drag-hover path (not a typing-latency hot path), sidebar workspace counts are small and bounded, and adding a cross-drag-session cache would introduce invalidation state for negligible benefit. The previously expensive per-hover `tabManagerFor` scan has been resolved by stashing the dragged workspace's pin state once at drag start rather than re-querying it on each hover event. Do not flag this recomputation as an unnecessary repeated scan.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3430
File: Sources/Sidebar/SidebarDropPlanner.swift:0-0
Timestamp: 2026-05-02T09:38:48.117Z
Learning: In manaflow-ai/cmux, `SidebarDropPlanner.workspaceAction(for:targets:)` treats a pointer position below all measured workspace rows as a valid "insert at end" drop: it computes `insertionIndex = legalNewWorkspaceInsertionIndex(orderedTargets.count, orderedTargets: orderedTargets)` and returns `.newWorkspace(insertionIndex:indicator:)`. Do NOT flag this path as missing — it is intentional and is covered by the updated `testWorkspaceDropAfterLastRowFallsThroughToEmptyAreaDropTarget` (or equivalent) test in `cmuxTests/SidebarWorkspaceDropPlannerTests.swift`.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3430
File: Sources/Sidebar/SidebarDropPlanner.swift:0-0
Timestamp: 2026-05-02T09:38:56.382Z
Learning: In manaflow-ai/cmux, `SidebarDropPlanner.workspaceAction(for:targets:)` maps pointer positions below all measured workspace rows to a `.newWorkspace` insertion at `orderedTargets.count` (end of list). It does NOT return `nil` for below-last-row drops. The behavior is covered by `testWorkspaceDropAfterLastRowCreatesWorkspaceAtEnd` in `cmuxTests/SidebarWorkspaceDropPlannerTests.swift`. (This supersedes any prior learning stating that nil was intentional for after-last-row drops.)

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3381
File: Sources/AppDelegate+MoveTabToNewWorkspace.swift:131-138
Timestamp: 2026-05-01T00:31:34.214Z
Learning: Repo: manaflow-ai/cmux — In Sources/AppDelegate+MoveTabToNewWorkspace.swift, `focusIntentForNewWorkspaceMove(panel:)` intentionally forces `.browser(.addressBar)` for `BrowserPanel` instances, overriding `preferredFocusIntentForActivation()`. This is by design: when a browser bonsplit tab is dragged into a standalone workspace, the omnibar must be surfaced regardless of the prior in-panel focus context. Do not flag this as a focus-stealing violation; the drag-to-new-workspace action is an explicit focus-intent command scoped to that path.

Learnt from: lawrencecchen
Repo: manaflow-ai/cmux PR: 3247
File: Sources/ContentView.swift:0-0
Timestamp: 2026-04-29T01:08:44.497Z
Learning: Repo: manaflow-ai/cmux — Behavior contract: Sidebar “Copy Workspace ID(s)” (e.g., in Sources/ContentView.swift TabItemView context menu) must copy plain UUIDs (IDs-only), not refs. Command palette identifier-copy commands should explicitly pass includeRefs: true when refs are desired. This split preserves backward compatibility for scripts while enabling richer payloads in the palette.

Learnt from: austinywang
Repo: manaflow-ai/cmux PR: 4265
File: Sources/TabManager.swift:3995-4055
Timestamp: 2026-05-18T10:44:27.336Z
Learning: In manaflow-ai/cmux, TabManager.reorderVisibleWorkspace(...) must preserve hidden-slot positions in the full tabs array. Implement by rebuilding tabs only across indices where workspace.isHidden == false (mapping reorderedVisibleIds onto those slots), and enforce pinned-at-top among visibles with clampedVisibleReorderIndex. Avoid using reorderWorkspace(before:/after:) directly for visible-only moves, since it can indirectly move interleaved hidden workspaces.
✅ Action performed

Full review finished.

This branch was successfully deployed

1 active deployment
Preview – cmux — 38f486b4 Deployed Jun 5, 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