Skip to content

Keep workspace group creation in place - #4989

Merged
lawrencecchen merged 24 commits into
mainfrom
task-group-position-stable
May 30, 2026
Merged

lawrencecchen merged 24 commits into
mainfrom
task-group-position-stable

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Keep newly-created workspace groups at the first selected workspace position instead of promoting them above ungrouped workspaces.
  • Add a model regression test for lower-row group creation.

Testing

  • ./scripts/reload.sh --tag grp-pos (passed)

Issues

  • Plain-text task: when making a group, the position shouldn't change

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


Note

Medium Risk
Touches core TabManager tab/group ordering and sidebar drag-drop paths used on every reorder; behavior is broad but heavily covered by new unit tests.

Overview
New groups stay where you grouped them instead of jumping above ungrouped workspaces. Creation snapshots the pre-group tab order, inserts the anchor at the first selected child, and normalizes contiguous runs without reshuffling unrelated top-level rows (placeNewWorkspaceGroupAtCreationPosition, optional normalizeWorkspaceGroupsAfterInsert on addWorkspace).

Sidebar ordering is modeled as top-level rows (solo workspaces + group anchors). TabManager adds scoped reorder ID/pinned helpers, reorderSidebarWorkspace / top-level moves, and rebuilds tabs[] via normalizeWorkspaceGroupRunsPreservingOrder while syncing workspaceGroups to anchor positions. Move-to-top, add-to-group, and group-slot moves were updated to preserve visible slots and pinned vs unpinned tiers.

Drag-and-drop routes drops through those scopes: SidebarDragState centralizes begin/clear/set indicator (including top-level mode), drop validation/planners use sidebarReorderWorkspaceIds, and commits call reorderSidebarWorkspace. Group headers reuse the tab drag payload—edge = reorder, center = add to group—with shared top indicators, an Equatable header, and removal of the separate group-reorder UTI/files.

Regression tests cover creation position, header/member drags, collapsed headers, and pin-tier behavior; workspace-groups docs describe the simplified two-tier sidebar layout.

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


Summary by cubic

Keep newly created workspace groups at the first selected workspace’s position and make sidebar reordering operate on top‑level rows. Group headers now drag like rows (edge = reorder, center = add) with scoped indicators and lazy, Equatable headers to reduce sidebar churn.

  • Bug Fixes

    • Scope header top/bottom and empty‑area/top indicators and validation to top‑level rows.
    • Preserve order across pinned/unpinned tiers; move‑to‑top respects tiers; adding to a group keeps the group’s slot.
    • Collapsed‑header edge drops reorder without grouping; same‑group center drops are no‑ops; same‑group header edge drops keep membership; consume no‑op top‑level drops to avoid flicker.
    • Keep a usable center drop zone at default and compact row heights.
  • New Features

    • Centralized top‑level reorder in TabManager (sidebarReorderWorkspaceIds, reorderSidebarWorkspace, pinned‑set helpers, clamped indices, projection back into tabs[]).
    • Stable group creation via placeNewWorkspaceGroupAtCreationPosition; addWorkspace(..., normalizeWorkspaceGroupsAfterInsert: Bool) keeps sections contiguous.
    • Group headers reuse the workspace tab drag payload and top drop indicator; removed group UTI/payload and Info.plist type. SidebarDragState adds beginDragging/setDropIndicator and tracks top‑level indicator usage.

Written for commit 1c1771b. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Enhanced sidebar drag-and-drop and reordering with scoped drop indicators and improved group-header dragging.
  • Bug Fixes

    • Group creation and reorders preserve original tab/top-level positions, respect pinned vs unpinned tiers, and avoid unnecessary reorder events.
  • Tests

    • Added tests covering group creation, header dragging, moving grouped children, pinned/unpinned interactions, and adding workspaces into groups.
  • Documentation

    • Clarified workspace-group pinning and sidebar ordering.

Review Change Stack

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@vercel

vercel Bot commented May 29, 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 May 30, 2026 7:50am
cmux-staging Building Building Preview, Comment May 30, 2026 7:50am

@coderabbitai

coderabbitai Bot commented May 29, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

addWorkspace gains a flag to skip post-insert normalization. createWorkspaceGroup captures pre-insert tab order, creates the anchor without normalizing, then repositions the new group via runs-preserving placement helpers that rebuild tabs as anchor-first contiguous runs. Sidebar drag/drop and indicator logic now use reorder-scoped id providers and header-measured drop delegates.

Changes

Workspace Group Ordering

Layer / File(s) Summary
Normalization parameter and guard
Sources/TabManager.swift
addWorkspace(...) adds normalizeWorkspaceGroupsAfterInsert: Bool = true; post-insert contiguity normalization is now conditional.
Group creation flow
Sources/TabManager.swift
createWorkspaceGroup captures originalTabOrder, creates the anchor with normalization disabled, and defers placement to placeNewWorkspaceGroupAtCreationPosition(...).
Slot-based group moves & apply-to-tabs
Sources/TabManager.swift
Group moves operate on group slots within pinned/unpinned tiers; applyWorkspaceGroupSlotOrderToTabs() projects the reordered slot sequence back to tabs.
Move-to-top adjustments
Sources/TabManager.swift
moveTabToTop and moveTabsToTop snapshot previous orders and use slot-move + slot-to-tabs projection or fallback normalization, posting workspace-order changes only when tabs actually change.
Placement & runs-preserving normalization
Sources/TabManager.swift
New helpers placeNewWorkspaceGroupAtCreationPosition(...) and normalizeWorkspaceGroupRunsPreservingOrder(_:) rebuild tabs by emitting each group as an anchor-first contiguous run; normalizeWorkspaceGroupContiguity() delegates to the runs-preserving routine.
Sidebar reorder APIs
Sources/TabManager.swift
Adds sidebarReorderWorkspaceIds(forDraggedWorkspaceId:), sidebarReorderPinnedWorkspaceIds(forDraggedWorkspaceId:), and reorderSidebarWorkspace(...) which clamp/reorder at top-level anchor tiers and sync workspaceGroups.
Sidebar drag/drop scope and UI wiring
Sources/ContentView.swift, Sources/SidebarWorkspaceGroupHeaderView.swift, Sources/VerticalTabsSidebar+WorkspaceGroups.swift, Sources/TabManager.swift
Extracts SidebarWorkspaceTopDropIndicator, computes reorder-scoped id lists for indicators and validation, tightens SidebarTabDropDelegate to the reorder scope, and wires header measurement plus delegate factories for group headers.
Unit tests
cmuxTests/WorkspaceGroupTests.swift
Five tests added to assert group-creation ordering preservation, header-drag reorder behavior and computed reorder ids, moving grouped children to the top preserves anchor placement (including pinned-tier cases), and adding a workspace into a group preserves the anchor's top-level position.
Project files & docs
Resources/Info.plist, cmux.xcodeproj/project.pbxproj, docs/workspace-groups.md
Removed sidebar group drag-payload source references and UTI export entry; updated pinning documentation to describe pinned vs unpinned top-level tiers.

🎯 4 (Complex) | ⏱️ ~45 minutes

  • manaflow-ai/cmux#4815: Prior grouped-workspace sidebar work that this PR continues and refactors, including earlier reorder and payload handling.

"I nibble at order with careful paws,
Anchors first, the runs each line obey,
Tabs shuffle soft, preserving outer laws,
A tiny hop restores the tidy way,
The rabbit hums — groups gleam at break of day."


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (2 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error applyWorkspaceGroupSlotOrderToTabs() rescans topLevelIds twice with filters and filters workspaceGroups twice in drag-drop hot paths, matching documented review concern. Partition top-level IDs and anchors in one pass instead of re-filtering both collections, per the review comment's proposed refactor.
Cmux Swiftui State Layout ❌ Error SidebarWorkspaceGroupHeaderView introduces @State rowHeight with GeometryReader that mutates state in onAppear/onChange callbacks (render-time state mutation) to measure row height for drop delegate. Replace rowHeightProbe with onGeometryChange modifier or pass measured height through a non-state mechanism to avoid render-time state writes.
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning PR description is incomplete relative to the provided template; missing Demo Video section, Review Trigger block, and Checklist items. Add Demo Video URL/link, copy Review Trigger block, and complete the Checklist with checkmarks or notes for each item (local testing, test coverage, docs, bot reviews, comment resolution).
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: newly-created workspace groups now stay at their original position instead of being promoted.
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 All new types follow Swift 6 actor isolation rules: SidebarWorkspaceGroupHeaderDropDelegate is @MainActor, TabManager methods on @MainActor class, Views implicitly MainActor.
Cmux Swift Blocking Runtime ✅ Passed New workspace group methods in TabManager and SidebarWorkspaceGroupHeaderView contain no blocking primitives; blocking patterns found are pre-existing baseline code.
Cmux No Hacky Sleeps ✅ Passed Check not applicable: PR modifies only Swift files, config, and docs; no TypeScript, JavaScript, shell, or runtime script changes covered by this check.
Cmux Swift Concurrency ✅ Passed PR removes legacy completion-handler APIs and adds no new Swift concurrency violations; new code uses synchronous methods and proper SwiftUI callback patterns.
Cmux Swift @Concurrent ✅ Passed All new methods are synchronous, properly @MainActor-isolated, and existing async functions have appropriate @concurrent annotations per rules.
Cmux Swift File And Package Boundaries ✅ Passed PR respects Swift file/package boundaries: TabManager +129 lines (<250 limit), ContentView +29 lines, SidebarWorkspaceGroupHeaderView 333 total (<400 limit), removes unnecessary drag-payload files.
Cmux Swift Logging ✅ Passed No violations detected: PR adds no print/debugPrint/dump/NSLog in production code, no ad hoc file logging, no improperly-declared Logger constants, and no secrets/tokens in logs.
Cmux User-Facing Error Privacy ✅ Passed No user-facing error messages, alerts, or unlocalized text exposing sensitive data. All strings use String(localized:defaultValue:) pattern; debug logs properly gated with #if DEBUG.
Cmux Full Internationalization ✅ Passed PR introduces no new user-facing strings; existing UI uses String(localized:) with full locale coverage; UTI identifier removal is exempt as literal token.
Cmux Architecture Rethink ✅ Passed No timing repairs, observers, or split lifecycle. Behavior consolidated with single TabManager owner and well-named invariants.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR adds no NSWindow, NSPanel, NSWindowController, Window, or WindowGroup types. New code is SwiftUI views and drop delegates; removed code is drag-payload types, not windows.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-group-position-stable

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.

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

🧹 Nitpick comments (1)
cmuxTests/WorkspaceGroupTests.swift (1)

43-59: ⚡ Quick win

Consider adding a count assertion for test completeness.

The test verifies specific positions 0-4 but doesn't assert that reorderedIds.count == 5. Adding this check would catch regressions where unexpected tabs are created during group creation.

🧪 Suggested assertion
 let reorderedIds = manager.tabs.map(\.id)
+#expect(reorderedIds.count == 5)

 `#expect`(reorderedIds[0] == originalIds[0])
🤖 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 `@cmuxTests/WorkspaceGroupTests.swift` around lines 43 - 59, The test
createGroupKeepsFirstChildPosition is missing an assertion on the total number
of tabs after grouping; update the test to assert the tab count by checking
reorderedIds.count equals 5 (use the existing reorderedIds variable from the
test on manager.tabs) before the position checks so the test will fail if extra
or missing tabs are present.
🤖 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.

Nitpick comments:
In `@cmuxTests/WorkspaceGroupTests.swift`:
- Around line 43-59: The test createGroupKeepsFirstChildPosition is missing an
assertion on the total number of tabs after grouping; update the test to assert
the tab count by checking reorderedIds.count equals 5 (use the existing
reorderedIds variable from the test on manager.tabs) before the position checks
so the test will fail if extra or missing tabs are present.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a5d1c826-3e5e-4baf-add5-c6966661db2d

📥 Commits

Reviewing files that changed from the base of the PR and between 8ea91b1 and c885981.

📒 Files selected for processing (2)
  • Sources/TabManager.swift
  • cmuxTests/WorkspaceGroupTests.swift

@greptile-apps

greptile-apps Bot commented May 29, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes workspace group creation jumping to the top of the sidebar by inserting the new group anchor at the first selected workspace's original position (placeNewWorkspaceGroupAtCreationPosition) and centralising all top-level reordering in a new reorderSidebarWorkspace/normalizeWorkspaceGroupRunsPreservingOrder path. Group headers now drag like regular rows (edge = reorder, center = add to group) using the shared SidebarTabDragPayload UTI, and the dedicated group-reorder UTI/payload is removed.

  • Group creation position: addWorkspace(normalizeWorkspaceGroupsAfterInsert: false) defers contiguity normalization so placeNewWorkspaceGroupAtCreationPosition can anchor the group at the first child's slot.
  • Top-level reorder: sidebarReorderWorkspaceIds/reorderSidebarWorkspace unify drag reorder for anchors and group members via normalizeWorkspaceGroupRunsPreservingOrder; applyWorkspaceGroupSlotOrderToTabs handles slot commands.
  • Header drag/drop: SidebarWorkspaceGroupHeaderDropDelegate delegates edge drops to SidebarTabDropDelegate and handles center drops as add-to-group; SidebarDragState gains dropIndicatorUsesTopLevelRows for scoped indicator visibility.

Confidence Score: 5/5

Safe to merge; ordering logic is well-tested at the model layer and drop-delegate changes are encapsulated behind static policy helpers.

No functional bugs found. Ordering algorithms are correct: orphaned group IDs are cleaned before dictionary lookups, the fallback loop in normalizeWorkspaceGroupRunsPreservingOrder ensures no tabs are silently dropped, clampedTopLevelReorderIndex correctly enforces pin-tier boundaries. The dropExited-without-dropEntered imbalance in SidebarWorkspaceGroupHeaderDropDelegate is harmless because the reorder delegate only clears the indicator when its own tabId matches. dropIndicatorUsesTopLevelRows is set and cleared consistently through setDropIndicator.

Sources/TabManager.swift continues to grow past an already substantial line count; the new ordering helpers have no UI dependencies and would benefit from extraction into a dedicated module over time.

Important Files Changed

Filename Overview
Sources/TabManager.swift Core ordering rewrite: adds sidebarReorderWorkspaceIds/reorderSidebarWorkspace/reorderTopLevelWorkspaceItem, placeNewWorkspaceGroupAtCreationPosition, normalizeWorkspaceGroupRunsPreservingOrder, and applyWorkspaceGroupSlotOrderToTabs. Logic is correct; file grows to 11,678 lines.
Sources/SidebarWorkspaceGroupHeaderView.swift Adds Equatable conformance for .equatable() suppression, GeometryReader rowHeight probe, onDrag/onDrop via SidebarWorkspaceGroupHeaderDropDelegate with center-vs-edge zone logic.
Sources/ContentView.swift SidebarDragState gains beginDragging/setDropIndicator/clearDropIndicator/clearDrag helpers and dropIndicatorUsesTopLevelRows flag; SidebarWorkspaceTopDropIndicator extracted as shared view.
Sources/VerticalTabsSidebar+WorkspaceGroups.swift Replaces old .onDrag/.onDrop on group headers with tabDropDelegateFactory; removes direct tabManager.tabs scans in favor of memberWorkspaceIds passed from renderItem.
cmuxTests/WorkspaceGroupTests.swift Adds 6 new model-level tests covering creation-position stability, header drag reorder, collapsed group edge/center drop, pinned clamping, collapsed render items, and top-level indicator scope.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[createWorkspaceGroup] -->|capture originalTabOrder| B[addWorkspace normalizeAfterInsert:false]
    B --> C[assignGroup to children + anchor]
    C --> D[placeNewWorkspaceGroupAtCreationPosition]
    D --> E{eligible children found?}
    E -- No --> F[normalizeWorkspaceGroupContiguity default order]
    E -- Yes --> G[build desiredIds: anchor+children at first-child slot]
    G --> H[topLevelWorkspaceIdsPreservingOrder]
    H --> I[normalizeWorkspaceGroupContiguity preservingTopLevelIds]
    I --> J[syncWorkspaceGroupsOrderToAnchorOrder]

    K[reorderSidebarWorkspace] --> L{usesTopLevelRows or dragged is anchor?}
    L -- Yes --> M[reorderTopLevelWorkspaceItem]
    L -- No --> N[reorderWorkspace flat path]
    M --> O[clampedTopLevelReorderIndex]
    O --> P[normalizeWorkspaceGroupRunsPreservingOrder]
    P --> Q[syncWorkspaceGroupsOrderToAnchorOrder]

    R[GroupHeader drop] --> S{center zone?}
    S -- Yes --> T[addWorkspaceToGroup]
    S -- No edge --> U[reorderDelegate.performDrop]
    S -- No-op edge --> V[consume drop clearDrag]
Loading

Reviews (18): Last reviewed commit: "Keep same-group header edge drops in gro..." | Re-trigger Greptile

Comment thread Sources/TabManager.swift
Comment on lines +2676 to +2677
autoWelcomeIfNeeded: Bool = true,
normalizeWorkspaceGroupsAfterInsert: Bool = 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 Boolean context flag on addWorkspace

normalizeWorkspaceGroupsAfterInsert is a caller-context flag that changes addWorkspace's internal contract: normalization only fires when the caller says so. The cmux-swift-architectural-rethink rule flags new Boolean flags wired through multi-purpose entrypoints because they make the function's single-invocation semantics implicit. The only current false call site is createWorkspaceGroup, which defers normalization to placeNewWorkspaceGroupAtCreationPosition. If a future caller forgets to pass false or forgets to normalize afterward, the group's tab order could end up in an inconsistent state. Consider encapsulating the deferred-normalization sequence inside createWorkspaceGroup without exposing the flag on the general addWorkspace surface.

Rule Used: Flag Swift fixes that patch symptoms while leaving... (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!

Comment thread Sources/TabManager.swift
Comment on lines +6456 to +6458
if workspaceGroups.contains(where: { $0.id == groupId }) {
syncWorkspaceGroupsOrderToAnchorOrder()
}

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 Transient tier-ordering deviation after group creation

After normalizeWorkspaceGroupRunsPreservingOrder rebuilds tabs[], an ungrouped workspace positioned before the first selected child will remain between ungrouped workspaces and the new group anchor, violating the unpinned-groups-before-ungrouped-unpinned tier until the next normalizeWorkspaceGroupContiguity call. Workspace-number shortcuts and next/previous navigation read tabs[] directly in this window. If the deviation is intentional, a comment here explaining it would help future readers; otherwise a guarded follow-up call to normalizeWorkspaceGroupContiguity() after the sync would close the gap.

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.

Documented this invariant at the creation-position helper. The stable-position path intentionally preserves the outer row order instead of applying the older all-groups-before-ungrouped tier normalization; callers that need tier ordering still use normalizeWorkspaceGroupContiguity().

— Claude Code

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

🤖 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 11569-11576: Compute sidebarReorderIds once in the parent view
instead of calling tabManager.sidebarReorderWorkspaceIds(forDraggedWorkspaceId:)
inside each row: when rendering the sidebar, compute let sidebarReorderIds =
tabManager.sidebarReorderWorkspaceIds(forDraggedWorkspaceId:
dragState.draggedTabId) (keyed by dragState.draggedTabId so it updates only when
the dragged id changes) and pass that array into each row; update usages that
currently call sidebarReorderWorkspaceIds (e.g., the code around
SidebarTabDropIndicatorPredicate.topVisible(forTabId:draggedTabId:dropIndicator:tabIds:))
to use the passed-in sidebarReorderIds so you avoid per-row recomputation and
the O(N²) behavior.

In `@Sources/SidebarWorkspaceGroupHeaderView.swift`:
- Around line 56-66: The onChange call in the rowHeightProbe GeometryReader uses
the deprecated single-parameter overload; update the call for macOS 14+ to use
onChange(of:initial:_:) by supplying the initial value and the two-parameter
closure form so changes to proxy.size.height update rowHeight correctly; locate
the GeometryReader in rowHeightProbe and replace .onChange(of:
proxy.size.height) { newHeight in ... } with the onChange(of: proxy.size.height,
initial: proxy.size.height) { _, newHeight in rowHeight = max(newHeight, 1) }
(ensuring rowHeight assignment logic remains the same).

In `@Sources/TabManager.swift`:
- Around line 6438-6448: The current projection building for desiredIds uses
removeFirst() on pinnedAnchors and unpinnedAnchors (pinnedAnchors,
unpinnedAnchors, desiredIds, topLevelIds, groupsByAnchorId, group.isPinned)
which yields O(n²) behavior; replace removeFirst() with simple index cursors
(e.g., pinnedIndex and unpinnedIndex) or Array.Iterator to read sequentially
from pinnedAnchors/unpinnedAnchors and increment the cursor when consuming an
element so desiredIds is built in linear time and moveWorkspaceGroup no longer
suffers quadratic behavior.
🪄 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: e977bba2-111c-4985-a556-7b2b9c64add4

📥 Commits

Reviewing files that changed from the base of the PR and between 2e5066e and 8634f0d.

📒 Files selected for processing (10)
  • Resources/Info.plist
  • Sources/ContentView.swift
  • Sources/SidebarTabDragPayload+WorkspaceGroups.swift
  • Sources/SidebarWorkspaceGroupDragPayload.swift
  • Sources/SidebarWorkspaceGroupHeaderView.swift
  • Sources/TabManager.swift
  • Sources/VerticalTabsSidebar+WorkspaceGroups.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/WorkspaceGroupTests.swift
  • docs/workspace-groups.md
💤 Files with no reviewable changes (4)
  • Resources/Info.plist
  • cmux.xcodeproj/project.pbxproj
  • Sources/SidebarWorkspaceGroupDragPayload.swift
  • Sources/SidebarTabDragPayload+WorkspaceGroups.swift

Comment thread Sources/ContentView.swift
Comment thread Sources/SidebarWorkspaceGroupHeaderView.swift
Comment thread Sources/TabManager.swift Outdated
Comment thread cmuxTests/WorkspaceGroupTests.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

Caution

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

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

6458-6475: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-tier top-level ids before projecting anchors.

Line 6460 builds the projection from the transient tabs order, but callers like moveTabToTop / moveTabsToTop temporarily bubble grouped members using Workspace.isPinned, not WorkspaceGroup.isPinned. That lets an unpinned group slot stay ahead of pinned groups after normalization.

♻️ Proposed fix
     private func applyWorkspaceGroupSlotOrderToTabs() {
         let groupsByAnchorId = Dictionary(uniqueKeysWithValues: workspaceGroups.map { ($0.anchorWorkspaceId, $0) })
-        let topLevelIds = sidebarTopLevelWorkspaceIds()
+        let topLevelIds = sidebarTopLevelWorkspaceIds()
+        let pinnedTopLevelIds = sidebarTopLevelPinnedWorkspaceIds()
+        let tierOrderedTopLevelIds =
+            topLevelIds.filter { pinnedTopLevelIds.contains($0) } +
+            topLevelIds.filter { !pinnedTopLevelIds.contains($0) }
         var pinnedAnchors = workspaceGroups.filter(\.isPinned).map(\.anchorWorkspaceId)
         var unpinnedAnchors = workspaceGroups.filter { !$0.isPinned }.map(\.anchorWorkspaceId)
-        let desiredIds = topLevelIds.map { id -> UUID in
+        let desiredIds = tierOrderedTopLevelIds.map { id -> UUID in
             guard let group = groupsByAnchorId[id] else { return id }
             if group.isPinned, !pinnedAnchors.isEmpty {
                 return pinnedAnchors.removeFirst()
🤖 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/TabManager.swift` around lines 6458 - 6475, The current
applyWorkspaceGroupSlotOrderToTabs() projects topLevelIds from the transient
tabs order without re-tiering pinned status, so grouped members bubbled via
Workspace.isPinned (by callers like moveTabToTop/moveTabsToTop) aren't reflected
and an unpinned WorkspaceGroup slot can remain ahead of pinned slots; fix by
re-tiering top-level IDs before projecting anchors: compute topLevelIds so that
you group/reorder top-level workspaces by the workspace-level pinned flag
(Workspace.isPinned) or by checking if any member in a WorkspaceGroup is pinned,
then build desiredIds from that re-tiered topLevelIds (update the variable
topLevelIds or its producer sidebarTopLevelWorkspaceIds() usage inside
applyWorkspaceGroupSlotOrderToTabs), then proceed with
normalizeWorkspaceGroupRunsPreservingOrder(desiredIds) and
syncWorkspaceGroupsOrderToAnchorOrder().
🤖 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 `@cmuxTests/WorkspaceGroupTests.swift`:
- Around line 93-110: The test
movingGroupedChildToTopKeepsAnchorFirstWhenGroupIsAlreadyFirst assumes three
tabs but makeTabManager() only creates two, causing an out-of-bounds access on
originalIds[2]; update the expected array in the `#expect` assertion to only
reference existing IDs (originalIds[0] and originalIds[1]) and the group's
anchorWorkspaceId so the assertion matches the two-tab setup after calling
manager.moveTabToTop(originalIds[1]); ensure you update the expected ordering in
the `#expect` check to remove originalIds[2] and reflect the correct reordered
sequence for manager.tabs.map(\.id).

In `@Sources/TabManager.swift`:
- Around line 5555-5557: The postWorkspaceOrderDidChange call is only sending
the explicitly selected tabId after moveWorkspaceGroupSlotToTierStart(...) which
moves the entire group slot; update both places (the block comparing
tabs.map(\.id) to previousOrder and the similar branch around
moveWorkspaceGroupSlotToTierStart at lines ~5586-5587) to compute and publish
all moved workspace ids (e.g., the ids of the entire group/slot that were
relocated) instead of just [tabId], by deriving the moved ids from the
group/slot being moved and passing that array into
postWorkspaceOrderDidChange(movedWorkspaceIds:).

---

Outside diff comments:
In `@Sources/TabManager.swift`:
- Around line 6458-6475: The current applyWorkspaceGroupSlotOrderToTabs()
projects topLevelIds from the transient tabs order without re-tiering pinned
status, so grouped members bubbled via Workspace.isPinned (by callers like
moveTabToTop/moveTabsToTop) aren't reflected and an unpinned WorkspaceGroup slot
can remain ahead of pinned slots; fix by re-tiering top-level IDs before
projecting anchors: compute topLevelIds so that you group/reorder top-level
workspaces by the workspace-level pinned flag (Workspace.isPinned) or by
checking if any member in a WorkspaceGroup is pinned, then build desiredIds from
that re-tiered topLevelIds (update the variable topLevelIds or its producer
sidebarTopLevelWorkspaceIds() usage inside applyWorkspaceGroupSlotOrderToTabs),
then proceed with normalizeWorkspaceGroupRunsPreservingOrder(desiredIds) and
syncWorkspaceGroupsOrderToAnchorOrder().
🪄 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: 04e02da9-b073-4abb-a055-b648743ae035

📥 Commits

Reviewing files that changed from the base of the PR and between 8634f0d and c523bc5.

📒 Files selected for processing (3)
  • Sources/SidebarWorkspaceGroupHeaderView.swift
  • Sources/TabManager.swift
  • cmuxTests/WorkspaceGroupTests.swift

Comment thread cmuxTests/WorkspaceGroupTests.swift
Comment thread Sources/TabManager.swift Outdated

@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/TabManager.swift`:
- Around line 6461-6465: The current implementation rescans topLevelIds and
workspaceGroups multiple times (calls to sidebarTopLevelPinnedWorkspaceIds(),
two topLevelIds.filter(...) and two workspaceGroups.filter(...)) inside
applyWorkspaceGroupSlotOrderToTabs(), causing extra work on the reorder hot
path; refactor to do a single pass that partitions topLevelIds into
pinnedTopLevelIds and unpinnedTopLevelIds and simultaneously partitions
workspaceGroups into pinnedAnchors and unpinnedAnchors (or build a Set of pinned
IDs from sidebarTopLevelPinnedWorkspaceIds() once and then loop
topLevelIds/workspaceGroups once) and then use those two partitioned collections
for the final projection so you avoid multiple full-array filters.
🪄 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: c9ed6d96-6f80-4b5b-9f85-2f1cc43c9faa

📥 Commits

Reviewing files that changed from the base of the PR and between c523bc5 and f6b3c14.

📒 Files selected for processing (5)
  • Sources/ContentView.swift
  • Sources/SidebarWorkspaceGroupHeaderView.swift
  • Sources/TabManager.swift
  • Sources/VerticalTabsSidebar+WorkspaceGroups.swift
  • cmuxTests/WorkspaceGroupTests.swift

Comment thread Sources/TabManager.swift Outdated

@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 cf0804e. Configure here.

Comment thread Sources/TabManager.swift
@lawrencecchen
lawrencecchen merged commit 3b711e4 into main May 30, 2026
19 checks passed
@lawrencecchen
lawrencecchen deleted the task-group-position-stable branch May 30, 2026 08:27
hhsw2015 pushed a commit to hhsw2015/cmux that referenced this pull request May 30, 2026
- Keep workspace group creation in place (manaflow-ai#4989)
- Allow narrower sidebar with titlebar accessories (manaflow-ai#5013)
- Add beta TextBox defaults settings (manaflow-ai#4773)
- Add default terminal registration (manaflow-ai#4935)
- Make group new workspace placement configurable (manaflow-ai#5018)
- Fix nightly publishing runner selection (manaflow-ai#5022)

Conflicts resolved:
- ContentView: take upstream's SidebarWorkspaceTopDropIndicator extraction.
- cmuxApp: keep fork's QuickTerminal/WorkspaceTopTabsVisibility @AppStorage
  + TerminalCopyOnSelectSettings; adopt upstream's Setting(\.terminal.*)
  for textBoxMaxLines + showTextBoxOnNewTerminals + focusTextBoxOnNewTerminals.
- xcstrings: keep both fork's settings.app.workspaceTopTabsVisibility[.*]
  keys and upstream's settings.app.workspaceGroupNewWorkspacePlacement.
- TabManagerUnitTests: drop fork's testNewSurfaceCreatesAndFocusesTopLevelTab
  (upstream renames + retains coverage via testNewSurfaceFocusesCreatedSurface).

This branch was successfully deployed

1 active deployment
Preview – cmux — 1c1771b8 Deployed May 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants