Skip to content

Keep pinned workspaces inside groups - #5541

Merged
lawrencecchen merged 5 commits into
mainfrom
issue-pinned-workspace-group-top
Jun 8, 2026
Merged

lawrencecchen merged 5 commits into
mainfrom
issue-pinned-workspace-group-top

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Keeps workspace pinning local inside a workspace group instead of ejecting the workspace from the group.
  • Sorts group rows as header, pinned children, then unpinned children.
  • Allows pinned workspaces through group creation, row context menu, shortcut, and socket add/create paths.

Tests

  • Added regression coverage in cmuxTests/WorkspaceGroupTests.swift.
  • jq empty Resources/Localizable.xcstrings
  • ./scripts/lint-pbxproj-test-wiring.sh
  • git diff --check origin/main..HEAD
  • ./scripts/reload-cloud.sh --tag pgrp

Dogfood

  • Built tag pgrp: http://127.0.0.1:17320/pgrp
  • In the tagged app, created workspace_group:1 from workspace:2 and workspace:3, pinned workspace:2, and confirmed the group payload stayed [workspace:4, workspace:2, workspace:3] with is_pinned=false.

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 ordering, pinning, and sidebar drag logic plus socket group APIs; behavior changes are broad but covered by new regression tests.

Overview
Pinned workspaces can remain group members instead of being ejected or blocking group actions. Pinning a grouped workspace reorders it to the pinned tier inside that group (anchor first, then pinned children, then unpinned), and drag/reorder paths clamp insertion with sidebarReorderLegalInsertionRange so members don’t cross anchor/pinned/unpinned boundaries—invalid drops suppress the indicator.

Group eligibility is aligned across the ⌘⇧G shortcut, row context menu, and socket/CLI: only existing group anchors are excluded (pinned is allowed). Socket failures use new localized strings when all explicit children are anchors or when adding a workspace that anchors another group.

Top-level sidebar pinning now treats only ungrouped pinned rows (and group-level pin state via isGlobalPinnedRow) as the global pinned segment; pinned members inside a group no longer count toward that leading block.

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


Summary by cubic

Pinned workspaces now stay inside their workspace group and reorder within it. Drag and drop is clamped so members can’t cross anchor/pinned/unpinned boundaries; top-level pinned rows ignore pinned group members.

  • New Features

    • Allow pinning grouped workspaces; groups render as anchor → pinned members → unpinned members.
    • Support pin/group actions via group creation, row menu, keyboard shortcut, and socket/CLI.
    • Drag and reorder keep members inside their group with legal insertion ranges that respect the pinned tier.
  • Bug Fixes

    • Prevent creating a group with explicit children when all requested children are anchors; return a localized error.
    • Adding to a group now errors when the workspace is the anchor of another group; pinned status no longer blocks this path.
    • Suppress/clamp drop indicators and target indices when an unpinned member is dragged over a pinned member in the same group.

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

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved sidebar pinning, group ordering, and drag/reorder behavior so pinned and grouped workspaces display and move more predictably; refined insertion clamping and drop indicator logic.
    • Tightened eligibility for creating/moving workspaces into groups to prevent invalid targets.
  • Documentation / Localization

    • Added two localized error messages for group creation/membership failures (English and Japanese).
  • Tests

    • Added tests covering pinning, grouping, and drag/reorder interactions.

@vercel

vercel Bot commented Jun 6, 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 8, 2026 3:12am
cmux-staging Building Building Preview, Comment Jun 8, 2026 3:12am

@coderabbitai

coderabbitai Bot commented Jun 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Allow pinned workspaces as group members and update related logic: localization, UI eligibility, ContentView→SidebarDropPlanner plumbing, TabManager reorder/pin and drag-inference, TerminalController validation, and tests.

Changes

Pinned Workspaces in Groups

Layer / File(s) Summary
Localization for workspace group errors
Resources/Localizable.xcstrings
Added English and Japanese localized error messages workspaceGroup.error.allChildrenAreAnchors and workspaceGroup.error.workspaceIsOtherGroupAnchor used by group-creation and add-to-group failure paths.
UI eligibility filters for group operations
Sources/AppDelegate.swift, Sources/TabItemView+WorkspaceGroups.swift
Shortcut and context-menu eligibility filters now allow pinned workspaces as candidates for group creation/membership by excluding existing group anchor IDs instead of filtering by isPinned.
Sidebar drop planning and ContentView plumbing
Sources/ContentView.swift, Sources/Sidebar/SidebarDropPlanner.swift
ContentView computes a legalInsertionRange and passes it into SidebarDropPlanner.indicator/targetIndex; SidebarDropPlanner APIs accept the optional range and final-clamp insertion positions to it after pinned/unpinned clamping.
TabManager reorder, pinned-state, and group inference
Sources/TabManager.swift
Pinned enumeration now excludes grouped pinned members for non-top-level reorder logic; drag-inferred group membership reads neighbor groupId directly; single-tab pin path no longer dissolves groups; multi-pin path early-returns when unchanged and reorders only changed IDs; reorder/clamping rewritten to use leading global pinned-row counting and grouped-member clamping; anchorFirst now emits anchor, then pinned non-anchors, then unpinned non-anchors.
TerminalController group operation validation
Sources/TerminalController.swift
Anchor-only child eligibility and add-to-group error messages now treat only existing group anchors as ineligible and use the new localized keys; pinned-specific failure branch removed.
Tests for pinned grouped members
cmuxTests/WorkspaceGroupTests.swift
Added tests verifying pinned non-anchor members stay at the top of their group's visible order (after the anchor), that pinning a member doesn't promote the whole group above globally pinned top-level workspaces, and that certain drag scenarios produce no drop indicator and correct target indices.

Sequence Diagram

sequenceDiagram
  participant User
  participant ContentView
  participant SidebarDropPlanner
  participant TabManager

  User->>ContentView: drag tab (draggedId, targetId)
  ContentView->>TabManager: sidebarReorderLegalInsertionRange(draggedId,targetId,usesTopLevelRows)
  ContentView->>SidebarDropPlanner: indicator(..., legalInsertionRange)
  SidebarDropPlanner->>TabManager: compute target index / clamped insertion position
  TabManager->>TabManager: reorder/pin inference, apply group membership
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#4989: Modifies TabManager's workspace-group creation/placement and pinned/group contiguity/eligibility logic.
  • manaflow-ai/cmux#4865: Refactors pinned/unpinned workspace ordering and setPinned/pin-state reorder logic that interacts with grouped/anchored ordering.

Suggested reviewers

  • Ari4ka

Poem

🐰 A hop, a pin, a group in place,

Anchors hold their honored space,
Pinned friends join without a race,
Reorders keep the sidebar's pace,
Tests confirm the tidy grace.


Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error Sources/TabManager.swift lines 5048-5062 contain nested full-collection scan: leadingGlobalPinnedRowCount() loops over tabs calling isGlobalPinnedRow() which does workspaceGroups.first(where:) scan. Cache workspaceGroupsById dictionary for O(1) group lookups instead of O(g), eliminating O(n*g) nested scan on UI hot paths.
Cmux Swift Logging ❌ Error Two of three added NSLog calls violate logging rules: line 9054 in production code, line 15449 in error path. Only line 9252 is acceptable (DEBUG-guarded). Remove or guard NSLog at lines 9054 and 15449 with #if DEBUG, or replace with Logger using unified logging system.
Cmux Full Internationalization ❌ Error Two new localization keys have incomplete translations: only 'en' and 'ja', missing 18 of 20 supported locales in Localizable.xcstrings. Add translated entries for all 20 supported locales to both workspaceGroup.error.allChildrenAreAnchors and workspaceGroup.error.workspaceIsOtherGroupAnchor in Resources/Localizable.xcstrings.
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Swift Actor Isolation ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Keep pinned workspaces inside groups' directly and clearly summarizes the main change: pinned workspaces remain group members instead of being ejected.
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 Blocking Runtime ✅ Passed NSLock and DispatchQueue.main.sync are DEBUG-only test scaffolding. Semaphores and Task.sleep are in unrelated socket/lifecycle code, not group management.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift and localization changes; rule applies only to TypeScript, JavaScript, shell, and non-Swift build/runtime scripts (Swift covered by separate rule).
Cmux Swift Concurrency ✅ Passed PR changes workspace group/pin logic without introducing legacy async patterns: no new DispatchQueue, fire-and-forget Tasks, @Published, .sink, or completion handlers added.
Cmux Swift @Concurrent ✅ Passed All PR changes modify only synchronous functions; no new async functions, @concurrent annotations, or nonisolated async work introduced. Complies with swift-concurrent-annotation.md.
Cmux Swift File And Package Boundaries ✅ Passed All changes stay under 250-line threshold for oversized files; no new files; no mixed responsibilities; feature appropriately in app target.
Cmux User-Facing Error Privacy ✅ Passed New error messages respect privacy rules: domain terminology (workspace/group/anchor), clear next steps, safe user-supplied UUIDs in data, no vendor names/credentials/internal details.
Cmux Swiftui State Layout ✅ Passed PR adds optional value parameter to SidebarDropPlanner without new @State/@Published/@observable. No store references in row subtrees, GeometryReader additions, or render-time mutations.
Cmux Architecture Rethink ✅ Passed PR is a correctness fix to keep pinned workspaces inside groups via TabManager normalization. No timing repairs, duplicate state ownership, split lifecycle, or symptom patching detected.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR modifies workspace group pinning logic and sidebar drag/drop, not window creation. No NSWindow, NSPanel, NSWindowController, Window, or WindowGroup added/materially changed.
Cmux Source Artifacts ✅ Passed All 8 changed files are intentional source code, tests, or hand-maintained localization catalogs with no prohibited artifacts or generated output detected.
Description check ✅ Passed PR description covers what changed and why, includes comprehensive testing details and dogfooding verification with demo data.
✨ 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-pinned-workspace-group-top

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.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c6e4836d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/TabManager.swift
Comment on lines +4092 to +4094
if tab.groupId != nil {
normalizeWorkspaceGroupContiguity()
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exclude grouped pins from batch global pin ordering

When this branch keeps a pinned workspace inside its group, the grouped child still has isPinned == true, but batchWorkspaceReorderFinalIds still buckets every isPinned workspace into the global pinned segment before normalizing group runs. In a state like A, group(anchor, pinned child), C, a workspace.reorder_many / CLI reorder of just C will first hoist the pinned child, then normalization emits the whole unpinned group before C, so an unrelated group jumps ahead of the requested item. The batch reorder path needs to treat grouped member pins like local group ordering, not global pinned rows.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jun 6, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR keeps pinned workspaces inside their workspace group instead of ejecting them, normalizes group member order to anchor → pinned children → unpinned children, and gates group-creation/membership eligibility on anchor status alone (not pin status).

  • TabManager core (TabManager.swift): reorderTabForPinnedState delegates to normalizeWorkspaceGroupContiguity for grouped members; new leadingGlobalPinnedRowCount/isGlobalPinnedRow helpers distinguish group-level pinning from member-level pinning; anchorFirst now enforces the three-tier sort; clampedGroupedMemberReorderIndex and sidebarReorderLegalInsertionRange clamp drag targets to keep members within their pinned/unpinned tier inside the group.
  • Eligibility checks (AppDelegate.swift, TabItemView+WorkspaceGroups.swift, TerminalController.swift): removed isPinned from group-creation/join filters; socket/CLI paths now return localized error strings via String(localized:) when all explicit children are anchors or when a workspace is another group's anchor.
  • Drop planning (SidebarDropPlanner.swift, ContentView.swift): indicator and targetIndex accept an optional legalInsertionRange that suppresses drop indicators and clamps the insert position when an unpinned member is dragged over a pinned peer in the same group.

Confidence Score: 5/5

Safe to merge; the new group-local pin semantics are well-isolated and covered by regression tests.

All eligibility checks, reorder paths, drag-clamp logic, and normalisation are internally consistent. The index alignment between sidebarReorderLegalInsertionRange (tabs indices) and SidebarDropPlanner (tabIds) is guaranteed because sidebarReorderWorkspaceIds returns tabs.map(.id) in the non-top-level-rows mode that the range function also requires. The three new tests cover the main new invariants. The only open items — missing locale translations for two new strings and the per-workspace normalise passes in batch-pin — were flagged in previous review rounds.

No files require special attention.

Important Files Changed

Filename Overview
Sources/TabManager.swift Core pin/group logic refactored: reorderTabForPinnedState, anchorFirst, clampedReorderIndex, and new helpers (leadingGlobalPinnedRowCount, isGlobalPinnedRow, clampedGroupedMemberReorderIndex, sidebarReorderLegalInsertionRange) all appear correct; index alignment with SidebarDropPlanner confirmed because sidebarReorderWorkspaceIds returns tabs.map(.id) in the non-top-level-rows mode that legalInsertionRange also requires.
Sources/Sidebar/SidebarDropPlanner.swift legalInsertionRange parameter added to indicator() and targetIndex(); applied after the existing pin-clamping pass; correctly suppresses drop indicators and clamps insert positions for in-group tier violations.
Sources/ContentView.swift sidebarReorderLegalInsertionRange called in both performDrop and dropUpdated handlers and threaded into SidebarDropPlanner; no issues found.
Sources/TerminalController.swift Removed isPinned-based no-op branches from workspace_group create and add paths; socket error messages migrated to String(localized:) with correct key/defaultValue pairs.
Resources/Localizable.xcstrings Two new error strings added with en+ja only; missing translations for the ~18 other locales already present in the catalog (previously noted).
cmuxTests/WorkspaceGroupTests.swift Three new regression tests cover: pinned member stays in group and sorts above unpinned peers, pinned member in unpinned group doesn't promote the group globally, and unpinned member dragged over pinned peer suppresses drop indicator.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[setPinned called] --> B{tab.groupId != nil?}
    B -- Yes --> C[normalizeWorkspaceGroupContiguity]
    C --> D[anchorFirst: anchor → pinned members → unpinned members]
    B -- No --> E[reorderTabForPinnedState ungrouped path]
    E --> F[leadingGlobalPinnedRowCount]
    F --> G{isGlobalPinnedRow per tab}
    G -- groupId set --> H[return group.isPinned]
    G -- no groupId --> I[return tab.isPinned]
    H & I --> J[insert at boundary]

    subgraph Drag Drop Clamping
    K[sidebarReorderLegalInsertionRange] --> L{dragged member is pinned?}
    L -- Yes --> M[range: firstIndex+1 .. firstIndex+1+pinnedCount]
    L -- No --> N[range: firstIndex+1+pinnedCount .. lastIndex+1]
    M & N --> O[SidebarDropPlanner clamps insertion to range]
    end

    subgraph Eligibility
    P[createWorkspaceGroup / addWorkspaceToGroup / shortcuts] --> Q{workspace is anchor of another group?}
    Q -- Yes --> R[reject / error]
    Q -- No --> S[allow, regardless of isPinned]
    end
Loading

Reviews (3): Last reviewed commit: "docs: update grouped drag pin comment" | Re-trigger Greptile

Comment on lines +166967 to +167000
"workspaceGroup.error.allChildrenAreAnchors": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "All requested children are ineligible because they are already group anchors; ungroup them first"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "要求された子ワークスペースはすべて既にグループのアンカーのため対象外です。先にグループ解除してください"
}
}
}
},
"workspaceGroup.error.workspaceIsOtherGroupAnchor": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Workspace is the anchor of another group; ungroup it first"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "このワークスペースは別のグループのアンカーです。先にグループ解除してください"
}
}
}
},

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.

P1 Incomplete localization for new error strings

Both workspaceGroup.error.allChildrenAreAnchors and workspaceGroup.error.workspaceIsOtherGroupAnchor only ship en and ja translations, but the catalog already supplies zh-Hans, zh-Hant, ko, de, es, fr, it, da, nb, pl, ru, pt-BR, ar, bs, th, tr, uk, and km for comparable workspace-group strings (e.g. contextMenu.workspaceGroup.moveTo carries all of those locales). Users running cmux in any of those other locales will receive the raw English text from the socket/CLI API instead of a localized message.

Rule Used: Flag production user-facing text that is not fully... (source)

Comment thread Sources/TabManager.swift
Comment on lines +4064 to 4072
if !workspaceGroups.isEmpty {
for id in changedIds {
if let workspace = workspacesById[id] {
reorderTabForPinnedState(workspace)
}
}
postWorkspaceOrderDidChange(movedWorkspaceIds: changedIds)
return changedIds
}

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 Batch pin/unpin calls normalizeWorkspaceGroupContiguity once per changed workspace

When workspaceGroups is non-empty, setPinnedWorkspaces now calls reorderTabForPinnedState in a loop; for any grouped member, that resolves to normalizeWorkspaceGroupContiguity(), which rebuilds the full tabs order. Pinning N grouped workspaces in one call therefore runs O(N) full-collection rebuild passes instead of the single pass the old code used. For typical usage this is imperceptible, but bulk socket/CLI commands that pin many grouped members at once will see linear slowdown relative to the batch size.

Rule Used: Flag production code that adds nested full-collect... (source)

Comment thread Sources/TabManager.swift

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e70ee31529

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/TabManager.swift
return Set(tabs.filter(\.isPinned).map(\.id))
return Set(tabs.filter { $0.groupId == nil && $0.isPinned }.map(\.id))
}
return sidebarTopLevelPinnedWorkspaceIds()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include promoted pinned children in top-level pin clamp

When a pinned child of an unpinned group is dragged over a group header, usesTopLevelRows promotes that child into the top-level reorder list, but this still returns only the already-visible top-level pins. Because the promoted child is still grouped at this point, it is omitted from the pin set, so the planner and reorderTopLevelWorkspaceItem can accept a bottom/unpinned target; after assignGroup(..., nil) runs, the workspace is now a standalone pinned row below unpinned rows, breaking the pin boundary. Include the promoted dragged workspace when it is pinned before computing the top-level pinned ids.

Useful? React with 👍 / 👎.

@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

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)

3818-3825: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Update the drag-inference contract comment.

The comment still describes the old "pinned workspaces never gain a group via drag" rule, but this implementation now intentionally allows pinned members inside groups.

🤖 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 3818 - 3825, Update the documentation
comment that begins "After a drag-driven reorder, infer the dragged workspace's
group membership..." to reflect the new behavior where pinned workspaces may be
placed inside groups via drag; remove or revise the sentence "Pinned workspaces
never gain a group via drag" and instead state that pinned members can be added
to groups when the drag-inference rules determine they belong inside a group's
section, keeping the rest of the neighbor-based rules unchanged so readers can
locate this logic in TabManager.swift around the drag-inference implementation.
🤖 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 3715-3751: The grouped-member bounds and pinned-tier calculations
in sidebarReorderLegalInsertionRange are duplicated across the pinned and
unpinned branches, so extract a helper (e.g.,
computeGroupMemberBounds(forGroupId:draggedWorkspaceId:)) that returns
(firstIndex: Int, lastIndex: Int, pinnedMemberCount: Int) by scanning tabs and
workspaceGroups using groupId, memberIndices and pinned checks; then use that
single helper to compute lower/upper in both the draggedWorkspace.isPinned
branch and the unpinned branch, and call the same helper from the other
duplicate code path referenced (lines ~5018-5044) so both reorderSidebar logic
and reorderWorkspace use identical bounds logic.

---

Outside diff comments:
In `@Sources/TabManager.swift`:
- Around line 3818-3825: Update the documentation comment that begins "After a
drag-driven reorder, infer the dragged workspace's group membership..." to
reflect the new behavior where pinned workspaces may be placed inside groups via
drag; remove or revise the sentence "Pinned workspaces never gain a group via
drag" and instead state that pinned members can be added to groups when the
drag-inference rules determine they belong inside a group's section, keeping the
rest of the neighbor-based rules unchanged so readers can locate this logic in
TabManager.swift around the drag-inference implementation.
🪄 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: 2103b96d-0ffc-439a-a263-97200a24d06c

📥 Commits

Reviewing files that changed from the base of the PR and between 7135f32 and e70ee31.

📒 Files selected for processing (8)
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Sidebar/SidebarDropPlanner.swift
  • Sources/TabItemView+WorkspaceGroups.swift
  • Sources/TabManager.swift
  • Sources/TerminalController.swift
  • cmuxTests/WorkspaceGroupTests.swift

Comment thread Sources/TabManager.swift
Comment on lines +3715 to +3751
func sidebarReorderLegalInsertionRange(
forDraggedWorkspaceId draggedWorkspaceId: UUID?,
targetWorkspaceId: UUID? = nil,
usesTopLevelRows: Bool = false
) -> ClosedRange<Int>? {
guard !usesTopLevelRows,
!sidebarReorderUsesTopLevelRows(
forDraggedWorkspaceId: draggedWorkspaceId,
targetWorkspaceId: targetWorkspaceId
),
let draggedWorkspaceId,
let draggedWorkspace = tabs.first(where: { $0.id == draggedWorkspaceId }),
let groupId = draggedWorkspace.groupId,
let group = workspaceGroups.first(where: { $0.id == groupId }),
draggedWorkspace.id != group.anchorWorkspaceId else {
return nil
}
let memberIndices = tabs.indices.filter { tabs[$0].groupId == groupId }
guard let firstIndex = memberIndices.first,
let lastIndex = memberIndices.last else {
return nil
}
let pinnedMemberCount = memberIndices.reduce(into: 0) { count, index in
let member = tabs[index]
if member.id != group.anchorWorkspaceId, member.isPinned {
count += 1
}
}
if draggedWorkspace.isPinned {
let lower = min(firstIndex + 1, tabs.count)
let upper = min(firstIndex + 1 + pinnedMemberCount, tabs.count)
return lower...max(lower, upper)
}
let lower = min(firstIndex + 1 + pinnedMemberCount, tabs.count)
let upper = min(lastIndex + 1, tabs.count)
return min(lower, upper)...max(lower, upper)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Extract the grouped-member bounds calculation into one helper.

These two paths now carry the same group scan and pinned-tier math. If one changes without the other, the sidebar will advertise a legal drop that reorderWorkspace cannot actually honor.

Also applies to: 5018-5044

🤖 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 3715 - 3751, The grouped-member bounds
and pinned-tier calculations in sidebarReorderLegalInsertionRange are duplicated
across the pinned and unpinned branches, so extract a helper (e.g.,
computeGroupMemberBounds(forGroupId:draggedWorkspaceId:)) that returns
(firstIndex: Int, lastIndex: Int, pinnedMemberCount: Int) by scanning tabs and
workspaceGroups using groupId, memberIndices and pinned checks; then use that
single helper to compute lower/upper in both the draggedWorkspace.isPinned
branch and the unpinned branch, and call the same helper from the other
duplicate code path referenced (lines ~5018-5044) so both reorderSidebar logic
and reorderWorkspace use identical bounds logic.

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

Comment thread Sources/TabManager.swift
}
tabs.remove(at: index)
let pinnedCount = tabs.filter { $0.isPinned }.count
let pinnedCount = leadingGlobalPinnedRowCount()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pin drops group normalize call

Medium Severity

Pinning or unpinning an ungrouped workspace no longer runs normalizeWorkspaceGroupContiguity when any workspace groups exist. reorderTabForPinnedState only inserts by leadingGlobalPinnedRowCount, which can place that workspace inside a group’s tabs[] run and break anchor-first contiguity until some later operation normalizes.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c079115. Configure here.

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

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)

3818-3826: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align the summary bullets with the actual membership inference logic.

At Line 3821-Line 3826, the comment says a single grouped neighbor can cause a join and “otherwise” the groupId is cleared, but the implementation preserves currentGroup when neighbors differ (Line 3863-Line 3867). Please keep the header bullets consistent with the real branch behavior.

Suggested doc-only fix
-    /// - If only one neighbor is in a group, join that neighbor's group when
-    ///   that group's anchor is the neighbor or another existing member
-    ///   (i.e. the dragged workspace sits "inside" the section).
-    /// - Otherwise, clear groupId.
+    /// - If both neighbors resolve to the same `groupId` (including `nil`),
+    ///   adopt that membership.
+    /// - If neighbors differ, keep the current membership (ambiguous edge drop).
🤖 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 3818 - 3826, Update the header comment
for the "After a drag-driven reorder" membership inference to match the
implementation: state that when both neighbors share a non-nil groupId the
dragged workspace joins that group, when only one neighbor is in a group it may
join that neighbor's group under the existing "anchor/inside section" condition,
and in the case where neighbors differ the code preserves the existing
currentGroup (does not clear groupId). Reference the variables/logic named
currentGroup and the neighbor-based branch that decides joining vs preserving so
the bullets mirror the actual behavior; leave the pinned-workspaces note as-is.
🤖 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.

Outside diff comments:
In `@Sources/TabManager.swift`:
- Around line 3818-3826: Update the header comment for the "After a drag-driven
reorder" membership inference to match the implementation: state that when both
neighbors share a non-nil groupId the dragged workspace joins that group, when
only one neighbor is in a group it may join that neighbor's group under the
existing "anchor/inside section" condition, and in the case where neighbors
differ the code preserves the existing currentGroup (does not clear groupId).
Reference the variables/logic named currentGroup and the neighbor-based branch
that decides joining vs preserving so the bullets mirror the actual behavior;
leave the pinned-workspaces note as-is.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 585427a2-0be8-491a-8dcf-063c7cdddd46

📥 Commits

Reviewing files that changed from the base of the PR and between e70ee31 and c079115.

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

This branch was successfully deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant