Skip to content

Add nested workspace folders - #6611

Closed
austinywang wants to merge 54 commits into
mainfrom
issue-6610-nested-workspace-folders
Closed

austinywang wants to merge 54 commits into
mainfrom
issue-6610-nested-workspace-folders

Conversation

@austinywang

@austinywang austinywang commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Generalizes workspace groups into a parent-pointer folder tree with WorkspaceGroup.parentGroupId and restore-stable SessionWorkspaceGroupSnapshot.parentGroupIndex fallback.
  • Renders workspace groups recursively in the sidebar with depth indentation, persisted collapse state, subtree-aware delete/collapse/mark-read behavior, and a localized New Folder context-menu action.
  • Adds drag-to-reparent for dropping a folder header onto another folder header, with cycle prevention in the shared model path.
  • Carries parent group IDs through session snapshots, crash pruning, control-socket group payloads, and mobile workspace-list payload/hash updates.

Design Notes

Validation

  • git diff --check
  • python3 -m json.tool Resources/Localizable.xcstrings >/dev/null
  • python3 scripts/check-package-resolved-policy.py
  • python3 scripts/check-workspace-package-groups.py --check
  • ./scripts/lint-pbxproj-test-wiring.sh

Per issue instructions, I did not run a local build, reload.sh, or xcodebuild.

Closes #6610
Refs #6514


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


Summary by cubic

Adds nested workspace folders with arbitrary depth on Mac and mobile, with subfolder creation, drag-to-reparent, depth-aware rendering with unread aggregation, and full persistence/restore. Refines nested drop planning and pin classification, improves mobile payloads/indentation, and aligns with #6610.

  • New Features

    • Model/Persistence: WorkspaceGroup.parentGroupId; snapshots store parentGroupId and restore-stable parentGroupIndex; restore walks up to the nearest surviving ancestor; create infers parent from selected children or accepts explicit parentGroupId.
    • APIs: control/mobile payloads include parent_group_id (control also sends parent_group_ref); mobile list hashing includes parent chain.
    • Drag & Reorder: center-drop on a folder header reparents folders; legal insertion ranges are scoped to the folder’s subtree; root-scoped edge-drops over nested headers target the root; child-boundary edge-drops target the parent’s sibling scope; pinned classification is by root folder; nested folder reorders publish all descendant workspace IDs.
    • UI/Mobile: recursive rendering with normalized depth and collapsed-unread aggregation; header menu adds “New Folder”; mobile rows carry depth for indentation; selecting a child scrolls to the nearest collapsed ancestor.
  • Bug Fixes

    • Restore/Crash: surviving folders re-link to the nearest surviving ancestor; standard restore mirrors crash pruning; missing anchors promote members or render grouped workspaces at root.
    • Rendering/Reorder: keep group hover scope even with a nil drop indicator; keep nested parent boundaries in subtree scope; fix root pin classification and nested slot ordering; preserve mixed child order; keep a parent’s top-level position for empty subfolder creates; guard edge-drops across parents; ignore stale group IDs when inferring a parent on create.
    • Sync: fix nested folder pin state and reparent propagation across control socket and mobile; mobile list hashing includes parent groups.

Written for commit 287c753. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Workspace groups support nesting with a parent-aware New Folder action.
    • Sidebar and mobile workspace lists render hierarchical indentation, nested headers at the anchor position, and collapse-aware visibility.
    • Mobile sync and control socket responses now include parent-group metadata; notification aggregates are group-aware.
  • Bug Fixes

    • Move/reorder, collapse selection, ungroup (dissolve), and delete now apply correctly to the entire group subtree.
    • Reparenting, pinned/unpinned ordering, and “moved workspace” reporting are hierarchy-aware.
    • Session restore and legacy decoding preserve nested parent chains safely.
  • Tests

    • Expanded regression and socket/drop/snapshot coverage for nested groups, reparenting, and subtree-scoped ordering.

@vercel

vercel Bot commented Jun 22, 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 Jul 4, 2026 8:21pm
cmux-staging Building Building Preview, Comment Jul 4, 2026 8:21pm

@coderabbitai

coderabbitai Bot commented Jun 22, 2026 •

Copy link
Copy Markdown

Review Change Stack

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

This PR adds nested workspace-group parent metadata, updates ordering and persistence to preserve parent chains, rewrites sidebar rendering and reorder behavior for hierarchical trees, and propagates the new structure through control, mobile, and test surfaces.

Changes

Nested Workspace Groups

Layer / File(s) Summary
Hierarchy contracts and snapshot fields
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Values/WorkspaceGroup.swift, Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/..., Sources/SessionPersistence.swift, Packages/macOS/CmuxControlSocket/..., Packages/iOS/CmuxMobileRPC/..., Packages/iOS/CmuxMobileShellModel/..., Sources/TerminalController+MobileWorkspaceList.swift, Sources/Mobile/MobileWorkspaceListObserver.swift, Resources/Localizable.xcstrings
Workspace-group models, snapshots, payloads, mobile previews, and localization add optional parent-group metadata and related serialization fields.
Root-aware ordering and invariant maintenance
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/..., Sources/SessionPersistencePolicy+CrashStorage.swift, Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/...
Group ordering, validation, dissolve behavior, ancestor expansion, crash-pruning, and drop legality now resolve roots, traverse subtrees, and relink parent chains.
Creation, reparenting, deletion, and reorder coordination
Sources/TabManager.swift, Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/..., Sources/AppDelegate.swift, Sources/TabItemView+WorkspaceGroups.swift, Sources/VerticalTabsSidebar+WorkspaceGroups.swift, Sources/TerminalController+ControlWorkspaceGroupContext.swift, Sources/SidebarDropIndicatorRowScopeResolver.swift, Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/...
Coordinator and drag/drop flows now create, move, delete, promote, and notify using subtree membership, parent-aware targets, and row-scope resolution.
Recursive sidebar rendering and mobile list output
Sources/SidebarWorkspaceRenderItem.swift, Sources/ContentView.swift, Sources/SidebarWorkspaceGroupHeaderView.swift, Sources/SidebarWorkspaceRenderChildRow.swift, Packages/macOS/CmuxSidebar/..., Packages/iOS/CmuxMobileShellModel/..., Packages/iOS/CmuxMobileShell/...
Sidebar and mobile list rendering now emit depth-aware recursive trees, aggregate unread state across descendants, and keep drag-state and teardown behavior aligned.
Nested-group regression coverage and project wiring
cmuxTests/*, Packages/macOS/CmuxWorkspaces/Tests/..., Packages/macOS/CmuxFoundation/Tests/..., Packages/macOS/CmuxSidebar/Tests/..., Packages/iOS/CmuxMobileRPC/Tests/..., cmux.xcodeproj/project.pbxproj
New and updated tests cover nested creation, reparenting, promotion, rendering, persistence, and drop planning, and the Xcode project registers the new sources and tests.
Estimated code review effort: 5 (Critical) ~120 minutes

Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 2 warnings)

Check name Status Explanation Resolution
Cmux Algorithmic Complexity ❌ Error Sidebar hot paths re-walk parent chains per item (WorkspacesModel+Ordering.swift:125-191, SidebarWorkspaceRenderItem.swift:93-122), so deep trees can go O(n²) with no benchmark note. Precompute root/normalized parent ids once per pass (memoized root lookup or parent map) and reuse them in ordering/rendering so sidebar passes stay linear.
Out of Scope Changes check ⚠️ Warning Some changes appear unrelated to nested folders, such as the MobileShellComposite deinit refactor and unrelated doc-comment cleanup. Split the unrelated MobileShellComposite cleanup and similar non-feature edits into a separate PR.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (22 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the nested-folder tree, parent links, recursive rendering, reparenting, persistence, and subtree-aware behavior requested by #6610.
Cmux Swift Actor Isolation ✅ Passed PASS: the new Sendable models stay plain value types; only UI helpers/views are @MainActor, and the added tests are MainActor too, so no new isolation debt.
Cmux Swift Blocking Runtime ✅ Passed Only touched production Swift code adjusts drop-hit geometry and subtree checks; no new waits, sleeps, syncs, polling, timers, or locks were introduced.
Cmux Browser Automation Off-Main ✅ Passed Diff doesn’t touch the rule-owned browser automation files and contains no browser wait/callback socket patterns, so no off-main violation is introduced.
Cmux Expensive Synchronous Load ✅ Passed Targeted diff checks show no added or moved RestorableAgentSessionIndex.load() / agent-history load in the PR; the existing closeWorkspace fallback is unchanged.
Cmux Cache Substitution Correctness ✅ Passed No new snapshot/history code swaps authoritative reads for stale caches; added parent-group snapshot data comes from live model state, and the cached agent-index path already falls back to a fresh...
Cmux No Hacky Sleeps ✅ Passed Only Swift source/test files changed; no TS/JS/shell/build runtime code added fixed sleeps, polling, or timers.
Cmux Swift Concurrency ✅ Passed No added diff lines introduce legacy async patterns; the matches were preexisting or test-boundary uses, not new production concurrency.
Cmux Swift @Concurrent ✅ Passed No changed Swift file adds invalid @concurrent or nonisolated async work; the new folder-tree changes are synchronous, and existing async UI work already hops off MainActor.
Cmux Swift File And Package Boundaries ✅ Passed No new oversized Swift production files or >250-line expansions; core nested-folder logic moved into CmuxWorkspaces/CmuxFoundation, leaving app-target code as thin UI/glue.
Cmux Swiftpm Lockfiles ✅ Passed HEAD changes only two Swift files; no .gitignore, Package.swift, Package.resolved, or Xcode package-reference edits are present, so the lockfile rule isn’t triggered.
Cmux Swift Logging ✅ Passed No added or modified Swift logging statements appeared in the PR diff; only preexisting logs remained untouched.
Cmux User-Facing Error Privacy ✅ Passed PASS: touched user-facing strings are generic (“New Folder”, workspace-group error copy) and don’t expose vendor/internal details or raw upstream messages.
Cmux Full Internationalization ✅ Passed PASS: The new "New Folder" string is localized via String(localized:) and the catalog entry has matching translations for all 20 locales; no new raw UI literals in production.
Cmux Swiftui State Layout ✅ Passed The SwiftUI diffs use immutable snapshots/closures; no new state objects, GeometryReader measurement changes, or render-time state writes were introduced.
Cmux Architecture Rethink ✅ Passed Centralized nested-folder state in model/coordinators; no new sleeps, locks, duplicate owners, or split lifecycle paths were introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No changed Swift file introduces or modifies NSWindow/NSPanel/WindowGroup code; diff is limited to sidebar drag/drop logic, so the auxiliary-window shortcut rule doesn’t apply.
Cmux Source Artifacts ✅ Passed Changed paths are intentional source/test/config/localization files; .github/swift-file-length-budget.tsv is a checked-in CI budget reference, not local artifact output.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Touched production files add nested-folder logic only; the only parent diff I found was SidebarWorkspaceReorderDropResolver, and it adds no new #if DEBUG/ForTesting seam.
Cmux No Ambient Global State ✅ Passed No new file-scope APIs, mutable globals, or singletons were added; the new helpers are instance members or stored-property model types.
Title check ✅ Passed The title clearly summarizes the main change: adding nested workspace folders.
Description check ✅ Passed The description is detailed and covers the main summary and validation, though the template’s Demo Video, Review Trigger, and Checklist sections are missing.
✨ 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-6610-nested-workspace-folders

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.

@greptile-apps

greptile-apps Bot commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR generalizes workspace groups into an arbitrary-depth parent-pointer folder tree. WorkspaceGroup gains parentGroupId, session snapshots store both UUID and index-stable parentGroupIndex for restore resilience, and rendering switches from a flat model to a recursive depth-aware DFS. Drag-to-reparent, subtree-aware collapse/delete/mark-read, mobile payload depth, and control-socket payloads all propagate the new parent chain.

  • Model & persistence: dual-redundant parentGroupIndex/parentGroupId in snapshots; crash-pruning and restore walk up the surviving parent chain to re-attach folders to their nearest surviving ancestor.
  • Rendering: SidebarWorkspaceRenderItem.renderItems replaced with an iterative DFS emitting groups with normalized depth; workspaceGroupNotificationSummaries computes subtree-aggregate unread counts via a memoized recursive helper.
  • Drop/reorder: SidebarWorkspaceReorderDropResolver extended with subtree-aware legal-insertion-range and center-drop reparent plan; moveWorkspaceGroupSlot scoped to same-parent same-pin-tier; cycle prevention is consistent in both the drop-resolver layer and the model layer.

Confidence Score: 5/5

Safe to merge. The nested folder tree is a large, well-scoped model change; cycle prevention, restore resilience, and subtree-aware actions are consistently applied across all relevant paths.

No correctness bugs found after tracing the DFS rendering path, the iterative normalization rewrite, the dual-redundant snapshot restore, crash-pruning parent re-link, and all four cycle-detection sites. The previously-flagged O(G²) normalization hot path is confirmed fixed. Only minor style observations filed.

The applyWorkspaceGroupSlotOrderToTabs rewrite in WorkspaceGroupCoordinator.swift and the memoized subtreeAggregate in ContentView.swift are the most non-trivial new paths and would benefit from a careful read in the first release after merge.

Important Files Changed

Filename Overview
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel+GroupInvariants.swift Iterative DFS rewrite of normalizeWorkspaceGroupRunsPreservingOrder; previously-flagged O(G²) hot path fixed by passing prebuilt groupsById into canSetWorkspaceGroupParent.
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift Adds setWorkspaceGroupParent, subtree-scoped deleteWorkspaceGroup with liveTabIds guard, child-promotion on ungroupWorkspaceGroup, same-parent slot scoping for moveWorkspaceGroupSlot.
Sources/SidebarWorkspaceRenderItem.swift Rewrites renderItems to iterative DFS with normalizedRenderableParentGroupId fallback; O(G + W) per render.
Sources/ContentView.swift Adds subtree-aggregate workspaceGroupNotificationSummaries in SwiftUI body; triple [key, default:] subscript pattern is a minor style issue; performWorkspaceReorderPlan extended for .reparentGroup.
Sources/SessionPersistencePolicy+CrashStorage.swift Crash-pruning correctly re-links surviving folders to nearest surviving ancestor; cycle-safe via visited set.
Sources/TabManager.swift Dual-redundant parent in session save; cycle-safe restore via wouldCreateParentCycle; commonWorkspaceGroupId returns nil across different groups.
Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver.swift Adds groupReparentPlan early return, nested-header edge/center semantics in groupScopeCandidate, root-group anchor fixes.
Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver+GroupLegalRange.swift New file: subtree-aware legalInsertionRange and renderedGroupScopedIndicator with correct pinned-slot detection.
Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver+GroupReparent.swift New file: groupReparentPlan, isCenterGroupHeaderDrop, canReparent with cycle prevention mirroring model layer.
Sources/SidebarDropIndicatorRowScopeResolver.swift New file: resolves drop-indicator scope from pre-rendered items via depth comparison; correctly includes header row.
Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceListItem.swift Rewrites renderItems with memoized subtree-unread and depth helpers; non-contiguous stray-member limitation is pre-existing and documented.
Sources/Mobile/MobileWorkspaceListObserver.swift Removes #if DEBUG seam by widening summaryHash to internal (canonical PR-6452 pattern); adds parentGroupId to hash.
Sources/TerminalController+ControlWorkspaceGroupContext.swift Adds peerIndex scoping before/after socket reorder to same-parent same-pin-tier; silent failure on tier mismatch is a minor diagnostic gap.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant SidebarDragDelegate
    participant DropResolver
    participant WorkspaceGroupCoordinator
    participant WorkspacesModel

    User->>SidebarDragDelegate: center-drop group A header onto group B header
    SidebarDragDelegate->>DropResolver: resolve(request, context)
    DropResolver->>DropResolver: groupReparentPlan() isCenterGroupHeaderDrop and canReparent check
    DropResolver-->>SidebarDragDelegate: "plan(.reparentGroup(A, B), indicator=nil, scope=.group(B))"
    SidebarDragDelegate->>WorkspaceGroupCoordinator: setWorkspaceGroupParent(A, B)
    WorkspaceGroupCoordinator->>WorkspacesModel: canSetWorkspaceGroupParent(A, B)
    WorkspacesModel-->>WorkspaceGroupCoordinator: true
    WorkspaceGroupCoordinator->>WorkspacesModel: "workspaceGroups[A].parentGroupId = B"
    WorkspaceGroupCoordinator->>WorkspacesModel: normalizeWorkspaceGroupContiguity()
    WorkspaceGroupCoordinator->>WorkspaceGroupCoordinator: workspaceGroupSubtreeWorkspaceIds(A)
    WorkspaceGroupCoordinator-->>SidebarDragDelegate: workspaceOrderDidChange(subtreeIds)
    Note over User,WorkspacesModel: Sidebar re-renders A nested under B at depth+1
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant SidebarDragDelegate
    participant DropResolver
    participant WorkspaceGroupCoordinator
    participant WorkspacesModel

    User->>SidebarDragDelegate: center-drop group A header onto group B header
    SidebarDragDelegate->>DropResolver: resolve(request, context)
    DropResolver->>DropResolver: groupReparentPlan() isCenterGroupHeaderDrop and canReparent check
    DropResolver-->>SidebarDragDelegate: "plan(.reparentGroup(A, B), indicator=nil, scope=.group(B))"
    SidebarDragDelegate->>WorkspaceGroupCoordinator: setWorkspaceGroupParent(A, B)
    WorkspaceGroupCoordinator->>WorkspacesModel: canSetWorkspaceGroupParent(A, B)
    WorkspacesModel-->>WorkspaceGroupCoordinator: true
    WorkspaceGroupCoordinator->>WorkspacesModel: "workspaceGroups[A].parentGroupId = B"
    WorkspaceGroupCoordinator->>WorkspacesModel: normalizeWorkspaceGroupContiguity()
    WorkspaceGroupCoordinator->>WorkspaceGroupCoordinator: workspaceGroupSubtreeWorkspaceIds(A)
    WorkspaceGroupCoordinator-->>SidebarDragDelegate: workspaceOrderDidChange(subtreeIds)
    Note over User,WorkspacesModel: Sidebar re-renders A nested under B at depth+1
Loading

Reviews (20): Last reviewed commit: "Ignore stale group ids for parent infere..." | Re-trigger Greptile

Comment thread Resources/Localizable.xcstrings

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

Caution

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

⚠️ Outside diff range comments (2)
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift (1)

557-570: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix the stale groupsByAnchorId reference.

Line 557 now declares rootGroupsByAnchorId, but Line 570 still reads groupsByAnchorId, which is undefined in this scope and will fail compilation.

Proposed fix
-            let isPinned = groupsByAnchorId[id]?.isPinned ?? (tabsById[id]?.isPinned == true)
+            let isPinned = rootGroupsByAnchorId[id]?.isPinned ?? (tabsById[id]?.isPinned == true)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift`
around lines 557 - 570, The variable created at the beginning is named
rootGroupsByAnchorId, but the code on line 570 references an undefined variable
groupsByAnchorId. In the line where isPinned is calculated within the for loop
that iterates over topLevelIds, replace the reference to groupsByAnchorId with
rootGroupsByAnchorId to match the variable name that was actually declared.
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift (1)

249-294: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Publish subtree moved IDs for root-folder sidebar reorders.

Line 249 sends root folder anchors through reorderTopLevelWorkspaceItem, but Lines 288-290 still publish only tabs whose groupId == group.id. When a root folder contains nested folders, the reorder moves the whole subtree while downstream consumers receive an incomplete movedWorkspaceIds payload.

Proposed fix
         let movedWorkspaceIds: [UUID]
         if let group = model.workspaceGroups.first(where: { $0.anchorWorkspaceId == tabId }) {
-            movedWorkspaceIds = model.tabs.filter { $0.groupId == group.id }.map(\.id)
+            movedWorkspaceIds = model.workspaceGroupSubtreeWorkspaceIds(groupId: group.id)
         } else {
             movedWorkspaceIds = [tabId]
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift`
around lines 249 - 294, The movedWorkspaceIds calculation in
reorderTopLevelWorkspaceItem does not include all descendants when a root folder
anchor with nested folders is reordered. The current logic at lines 288-290
filters only tabs whose groupId matches the direct group, but when a root folder
anchor is moved, the entire subtree of descendants (including nested folders and
their children) should be included in the movedWorkspaceIds published to the
host. Update the movedWorkspaceIds calculation to recursively collect all
descendant workspace IDs when the tabId is an anchor with a group, ensuring the
complete moved subtree is communicated via the host?.workspaceOrderDidChange
call.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift`:
- Around line 51-53: The resolvedParentGroupId calculation using flatMap
silently converts an invalid parentGroupId to nil instead of rejecting it,
violating the documented contract that requires either an existing folder or nil
root. Replace the current flatMap logic in the resolvedParentGroupId assignment
with explicit validation that throws an error or returns early when
parentGroupId is provided but does not exist in model.workspaceGroups, ensuring
invalid parent references are rejected rather than silently treated as root
folder requests.

In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel`+GroupInvariants.swift:
- Around line 118-122: The while loop that walks ancestors in the selection
didSet path rescans workspaceGroups on each iteration using firstIndex(where:),
which becomes inefficient for deep trees. Create a dictionary mapping group ids
to their indices in workspaceGroups before the while loop starts (before cursor
is initialized), then replace the firstIndex(where:) call inside the loop with a
simple dictionary lookup using the current UUID. This ensures the ancestor walk
remains linear time regardless of tree depth.
- Around line 45-53: The appendGroupSubtree function uses recursion to traverse
nested workspace groups, which can cause stack overflow with arbitrary-depth
folder hierarchies. Convert this to an iterative approach by introducing an
explicit stack variable to manage the traversal instead of using recursive
calls. Initialize the stack with the initial group parameter, then loop while
the stack is not empty, processing one group at a time. When you encounter child
groups via childGroupsByParentId, push them onto the stack in reverse order (so
they are processed in the original sibling order when popped from the stack).
Keep all the existing logic for tracking emitted group and workspace IDs and
appending members in the same order.
- Around line 92-95: The loop iterating over workspaceGroups.indices repeatedly
calls canSetWorkspaceGroupParent, which rebuilds internal lookup structures on
each iteration, creating O(groups²) complexity. Build a groupsById map once
before the loop, add an overload of canSetWorkspaceGroupParent that accepts this
map as a parameter, and update the loop to call the new overload instead of the
original. Update the groupsById map when a parent is cleared during
normalization to keep the map current for subsequent validation calls.

In
`@Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift`:
- Around line 327-347: The test function reparentWorkspaceGroupRejectsCycles()
and the other related test functions mentioned in the comment contain force-try
operators in the try! `#require` calls, which triggers SwiftLint errors. To fix
this, convert each test function to a throwing function by adding throws to the
function signature (for example, change func
reparentWorkspaceGroupRejectsCycles() to func
reparentWorkspaceGroupRejectsCycles() throws), then replace all instances of
try! `#require` with try `#require` throughout these test methods. Apply this change
to reparentWorkspaceGroupRejectsCycles() and all other test functions mentioned
in the also applies to comment.

In `@Sources/SessionPersistencePolicy`+CrashStorage.swift:
- Around line 295-313: The survivingParentId function currently only walks
ancestors using parentGroupId, but it should also respect the parentGroupIndex
fallback that restoreSessionSnapshot uses to reconstruct parent relationships.
Modify the survivingParentId function to check parentGroupIndex first (to find
parents the same way restore does), and only use parentGroupId as a fallback.
This prevents crash pruning from flattening folder hierarchies that depend on
the index-based parent resolution before restoreSessionSnapshot can reconstruct
them properly.

In `@Sources/SidebarWorkspaceRenderItem.swift`:
- Around line 123-139: The appendChildren function performs a full scan of the
tabs collection for each parent group, causing O(n²) complexity in nested trees.
Precompute a dictionary once that groups tabs by their normalized parent group
id using normalizedParentGroupId(for:), storing the result in a property similar
to how anchorGroupByWorkspaceId is structured. Then modify appendChildren to
directly access the precomputed bucket for the given parentGroupId instead of
looping through all tabs each time, eliminating the redundant scans over the
entire collection.

In `@Sources/TabManager.swift`:
- Around line 1873-1888: The commonWorkspaceGroupId method rescans the entire
tabs collection for each workspace ID in the input array, creating O(n*m)
complexity. Build a dictionary once at the start of the method that maps
workspace IDs to their corresponding tab objects using tabs.reduce or a similar
approach, then use this dictionary for constant-time lookups within the loop
instead of calling tabs.first(where:) repeatedly for each workspaceId.
- Around line 6096-6114: The parentByGroupId dictionary is being rebuilt on
every iteration inside the loop over restored.indices, causing O(groups²)
complexity. Move the initialization of parentByGroupId outside and before the
loop that processes each group, then pass it as a parameter to the
wouldCreateParentCycle function instead of rebuilding it internally. Update the
parentByGroupId dictionary with the new parent assignment only when the cycle
check passes (inside the if block where restored[index].parentGroupId is being
set), so the dictionary reflects the currently accepted state.

---

Outside diff comments:
In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift`:
- Around line 557-570: The variable created at the beginning is named
rootGroupsByAnchorId, but the code on line 570 references an undefined variable
groupsByAnchorId. In the line where isPinned is calculated within the for loop
that iterates over topLevelIds, replace the reference to groupsByAnchorId with
rootGroupsByAnchorId to match the variable name that was actually declared.

In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift`:
- Around line 249-294: The movedWorkspaceIds calculation in
reorderTopLevelWorkspaceItem does not include all descendants when a root folder
anchor with nested folders is reordered. The current logic at lines 288-290
filters only tabs whose groupId matches the direct group, but when a root folder
anchor is moved, the entire subtree of descendants (including nested folders and
their children) should be included in the movedWorkspaceIds published to the
host. Update the movedWorkspaceIds calculation to recursively collect all
descendant workspace IDs when the tabId is an anchor with a group, ensuring the
complete moved subtree is communicated via the host?.workspaceOrderDidChange
call.
🪄 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: 61ec573c-f0df-447d-8aa6-355b27c44835

📥 Commits

Reviewing files that changed from the base of the PR and between 51136fb and 386508c.

📒 Files selected for processing (25)
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/WorkspaceGroup/ControlCommandCoordinator+WorkspaceGroup.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/WorkspaceGroup/ControlWorkspaceGroupSnapshot.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel+GroupInvariants.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel+Ordering.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Values/WorkspaceGroup.swift
  • Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/ContentView.swift
  • Sources/Mobile/MobileWorkspaceListObserver.swift
  • Sources/SessionPersistence.swift
  • Sources/SessionPersistencePolicy+CrashStorage.swift
  • Sources/SidebarWorkspaceGroupHeaderDropDelegate.swift
  • Sources/SidebarWorkspaceGroupHeaderView.swift
  • Sources/SidebarWorkspaceRenderItem.swift
  • Sources/TabItemView+WorkspaceGroups.swift
  • Sources/TabManager.swift
  • Sources/TerminalController+ControlWorkspaceGroupContext.swift
  • Sources/TerminalController+MobileWorkspaceList.swift
  • Sources/VerticalTabsSidebar+WorkspaceGroups.swift
  • cmuxTests/MobileWorkspaceListFidelityTests.swift
  • cmuxTests/SidebarWorkspaceDropPlannerTests.swift
  • cmuxTests/WorkspaceGroupTests.swift

Comment thread Sources/SessionPersistencePolicy+CrashStorage.swift
Comment thread Sources/SidebarWorkspaceRenderItem.swift
Comment thread Sources/TabManager.swift
Comment thread Sources/TabManager.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.

Caution

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

⚠️ Outside diff range comments (1)
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel+Ordering.swift (1)

140-146: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep promoted pinned items in the pinned tier.

sidebarTopLevelWorkspaceIds(promotingWorkspaceId:) can insert a nested group anchor or grouped workspace before it is actually top-level, but clampedTopLevelReorderIndex still derives pinnedIds from the pre-promotion top-level rows. A pinned nested folder/member dragged out can therefore be clamped as unpinned and persisted below unpinned roots via the top-level reorder path.

Proposed fix
         let pinnedCount = topLevelIds.reduce(into: 0) { count, id in
             if pinnedIds.contains(id) {
                 count += 1
             }
         }
-        if pinnedIds.contains(workspaceId) {
-            return min(clamped, max(0, pinnedCount - 1))
+        let alreadyPinnedTopLevel = pinnedIds.contains(workspaceId)
+        let promotedPinnedTopLevel: Bool
+        if let group = workspaceGroups.first(where: { $0.anchorWorkspaceId == workspaceId }) {
+            promotedPinnedTopLevel = group.parentGroupId != nil && group.isPinned
+        } else if let tab = tabs.first(where: { $0.id == workspaceId }) {
+            promotedPinnedTopLevel = tab.groupId != nil && tab.isPinned
+        } else {
+            promotedPinnedTopLevel = false
+        }
+        if alreadyPinnedTopLevel || promotedPinnedTopLevel {
+            let lastPinnedIndex = pinnedCount - (alreadyPinnedTopLevel ? 1 : 0)
+            return min(clamped, max(0, lastPinnedIndex))
         }
         return max(clamped, pinnedCount)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel`+Ordering.swift
around lines 140 - 146, The issue is that when promoting a pinned nested group
or workspace in the sidebarTopLevelWorkspaceIds(promotingWorkspaceId:) method,
the insertion logic does not preserve its pinned status. When inserting the
promotedWorkspaceId at the calculated position using min(groupIndex + 1,
ids.count), the method can place a pinned item below the pinned tier boundary,
causing it to be treated as unpinned. To fix this, check whether the
promotedWorkspaceId was originally in the pinned items, and if so, constrain the
insertion index to not exceed the pinnedCount boundary to ensure promoted pinned
items remain in the pinned tier during reordering.
🤖 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
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel`+Ordering.swift:
- Around line 140-146: The issue is that when promoting a pinned nested group or
workspace in the sidebarTopLevelWorkspaceIds(promotingWorkspaceId:) method, the
insertion logic does not preserve its pinned status. When inserting the
promotedWorkspaceId at the calculated position using min(groupIndex + 1,
ids.count), the method can place a pinned item below the pinned tier boundary,
causing it to be treated as unpinned. To fix this, check whether the
promotedWorkspaceId was originally in the pinned items, and if so, constrain the
insertion index to not exceed the pinnedCount boundary to ensure promoted pinned
items remain in the pinned tier during reordering.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 03de335b-d96f-40ed-a6e7-196b6d73692a

📥 Commits

Reviewing files that changed from the base of the PR and between 386508c and 15b35eb.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (15)
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileSyncWorkspaceListResponse.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileWorkspacePreview+RemoteMapping.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCClientTests.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceGroupPreview.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceListItem.swift
  • Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspaceListItemTests.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel+GroupInvariants.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel+Ordering.swift
  • Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift
  • Resources/Localizable.xcstrings
  • Sources/SessionPersistencePolicy+CrashStorage.swift
  • Sources/SidebarWorkspaceRenderItem.swift
  • Sources/TabManager.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: 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
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift`:
- Around line 275-282: Remove the else if branch that checks
promotedTab?.isPinned when promotedGroupIndex is nil. Root groups should only be
classified as pinned based on their own WorkspaceGroup.isPinned state, not by
their anchor tab's pin state. This prevents unpinned root groups whose anchor
workspace is pinned from being incorrectly placed in the pinned tier during drag
reorder. Apply the same fix to all similar occurrences where this condition is
present.
🪄 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: e7652931-ed26-4dc9-a342-98dec7775157

📥 Commits

Reviewing files that changed from the base of the PR and between 15b35eb and 71fbb2d.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (6)
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel+Ordering.swift
  • Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift
  • Sources/SidebarWorkspaceRenderItem.swift
  • Sources/VerticalTabsSidebar+WorkspaceGroups.swift
  • 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.

Caution

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

⚠️ Outside diff range comments (4)
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift (2)

355-357: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Increment the count for the last-workspace holdout.

This branch removes the last remaining workspace from the deleted group but returns without incrementing closed, despite the comment saying it should be reported as removed. A single-workspace group deletion can therefore return 0.

Suggested fix
             if model.tabs.count <= 1 {
                 model.assignGroup(workspaceId: tab.id, groupId: nil)
+                closed += 1
                 continue
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift`
around lines 355 - 357, In the WorkspaceGroupCoordinator.swift file, the branch
handling when model.tabs.count <= 1 removes a workspace from the deleted group
by calling model.assignGroup(workspaceId: tab.id, groupId: nil) but fails to
increment the closed counter before continuing. Add an increment to the closed
counter after the assignGroup call and before the continue statement to properly
track that this workspace removal should be reported as removed in the final
count.

531-612: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Project nested group slot moves, or reject them.

moveWorkspaceGroupSlot can reorder groups with a non-nil parentGroupId, but applyWorkspaceGroupSlotOrderToTabs only builds a root-group projection. For nested sibling moves, normalization ranks child rows by the old tab order, then syncWorkspaceGroupsOrderToAnchorOrder() restores the old workspaceGroups order, so the move can report success without moving the folder.

Either make the slot projection include nested sibling anchors in the reordered parent’s child order, or explicitly return false for non-root group slot moves until that projection exists.

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

In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift`
around lines 531 - 612, The issue is that moveWorkspaceGroupSlot allows
reordering groups with non-nil parentGroupId (nested groups), but
applyWorkspaceGroupSlotOrderToTabs only builds a projection for root groups,
causing nested moves to report success without actually reordering. Fix this by
adding a guard check at the beginning of moveWorkspaceGroupSlot that returns
false when the source group has a non-nil parentGroupId, explicitly rejecting
nested group slot moves until the nested projection logic is implemented in
applyWorkspaceGroupSlotOrderToTabs.
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift (1)

186-198: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mirror promotion-aware pinning in the drag planner.

The commit path adds a promoted pinned nested group/workspace to pinnedTopLevelIds, but this planner helper returns only currently top-level pinned rows. SidebarDropPlanner can therefore compute an unpinned-tier target for a dragged pinned nested folder/workspace before commit clamps it elsewhere.

Suggested fix
         guard usesTopLevelRows || sidebarReorderUsesTopLevelRows(
             forDraggedWorkspaceId: draggedWorkspaceId,
             targetWorkspaceId: targetWorkspaceId
         ) else {
             return Set(model.tabs.filter { $0.groupId == nil && $0.isPinned }.map(\.id))
         }
-        return model.sidebarTopLevelPinnedWorkspaceIds()
+        var pinnedIds = model.sidebarTopLevelPinnedWorkspaceIds()
+        if let draggedWorkspaceId,
+           !pinnedIds.contains(draggedWorkspaceId) {
+            if let promotedGroup = model.workspaceGroups.first(where: {
+                $0.anchorWorkspaceId == draggedWorkspaceId && $0.parentGroupId != nil
+            }) {
+                if promotedGroup.isPinned {
+                    pinnedIds.insert(draggedWorkspaceId)
+                }
+            } else if let tab = model.tabs.first(where: { $0.id == draggedWorkspaceId }),
+                      tab.groupId != nil,
+                      !model.isWorkspaceGroupAnchor(draggedWorkspaceId),
+                      tab.isPinned {
+                pinnedIds.insert(draggedWorkspaceId)
+            }
+        }
+        return pinnedIds
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift`
around lines 186 - 198, The sidebarReorderPinnedWorkspaceIds method currently
returns only currently top-level pinned rows but does not account for promoted
pinned nested workspaces/groups, which causes the drag planner to compute
incorrect target positions. Modify the logic in sidebarReorderPinnedWorkspaceIds
to include promoted pinned nested items in addition to the currently top-level
pinned ones, ensuring that both the unpinned-tier return path (in the guard else
block) and the usesTopLevelRows path mirror the promotion-aware pinning behavior
that the commit operation applies.
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel+GroupInvariants.swift (1)

306-329: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the dissolve comment to match parent promotion.

Lines 320-329 now promote direct members and child groups to the dissolved group’s former parent, but the doc comment still says members become ungrouped. That documents the old flat-group invariant.

Suggested comment update
-    /// remaining members lose their `groupId` and stay in `tabs` as ungrouped
-    /// workspaces. Caller is responsible for having already removed the closed
-    /// workspace from `tabs`.
+    /// remaining members and direct child groups are promoted to the dissolved
+    /// group's former parent, or to the sidebar root when there is no parent.
+    /// Caller is responsible for having already removed the closed workspace
+    /// from `tabs`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel`+GroupInvariants.swift
around lines 306 - 329, The documentation comment for the
dissolveGroupsAnchoredBy method states that remaining members become ungrouped,
but the actual implementation (lines 320-329) promotes them to the dissolved
group's parent instead using parentByDissolvedGroupId. Update the doc comment
above the method to accurately reflect that when a group is dissolved, its
direct members and child groups are promoted to the dissolved group's former
parent group, rather than becoming ungrouped. This will ensure the documentation
matches the current behavior of the promotion logic.
🤖 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
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift`:
- Around line 355-357: In the WorkspaceGroupCoordinator.swift file, the branch
handling when model.tabs.count <= 1 removes a workspace from the deleted group
by calling model.assignGroup(workspaceId: tab.id, groupId: nil) but fails to
increment the closed counter before continuing. Add an increment to the closed
counter after the assignGroup call and before the continue statement to properly
track that this workspace removal should be reported as removed in the final
count.
- Around line 531-612: The issue is that moveWorkspaceGroupSlot allows
reordering groups with non-nil parentGroupId (nested groups), but
applyWorkspaceGroupSlotOrderToTabs only builds a projection for root groups,
causing nested moves to report success without actually reordering. Fix this by
adding a guard check at the beginning of moveWorkspaceGroupSlot that returns
false when the source group has a non-nil parentGroupId, explicitly rejecting
nested group slot moves until the nested projection logic is implemented in
applyWorkspaceGroupSlotOrderToTabs.

In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift`:
- Around line 186-198: The sidebarReorderPinnedWorkspaceIds method currently
returns only currently top-level pinned rows but does not account for promoted
pinned nested workspaces/groups, which causes the drag planner to compute
incorrect target positions. Modify the logic in sidebarReorderPinnedWorkspaceIds
to include promoted pinned nested items in addition to the currently top-level
pinned ones, ensuring that both the unpinned-tier return path (in the guard else
block) and the usesTopLevelRows path mirror the promotion-aware pinning behavior
that the commit operation applies.

In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel`+GroupInvariants.swift:
- Around line 306-329: The documentation comment for the
dissolveGroupsAnchoredBy method states that remaining members become ungrouped,
but the actual implementation (lines 320-329) promotes them to the dissolved
group's parent instead using parentByDissolvedGroupId. Update the doc comment
above the method to accurately reflect that when a group is dissolved, its
direct members and child groups are promoted to the dissolved group's former
parent group, rather than becoming ungrouped. This will ensure the documentation
matches the current behavior of the promotion logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e28524bf-50eb-42c9-bf30-121d1b2c73e8

📥 Commits

Reviewing files that changed from the base of the PR and between 71fbb2d and a164b09.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (6)
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceReorderCoordinator.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Model/WorkspacesModel+GroupInvariants.swift
  • Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift
  • Sources/ContentView.swift
  • 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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift`:
- Around line 61-63: The SwiftUI state backing the chat-refresh ignore logic is
currently exposed as non-private; update the `WorkspaceDetailView` `@State`
properties `ignoredChatSessionRefreshKey`, `ignoredChatSessionRefreshID`, and
`ignoredChatSessionRefreshTask` to be private. Keep the
`Task<[ChatSessionDescriptor]?, Never>` type unchanged, and only adjust access
control for these internal state members since they are not used as external
bindings.
🪄 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: 033a12d0-922f-46ff-924f-c33253918037

📥 Commits

Reviewing files that changed from the base of the PR and between f704023 and 36732c8.

📒 Files selected for processing (13)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver.swift
  • Packages/macOS/CmuxFoundation/Tests/CmuxFoundationTests/SidebarDropPlannerTests.swift
  • Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Drag/SidebarDragState.swift
  • Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/SidebarDragStateTests.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift
  • Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceGroupUngroupPromotionTests.swift
  • Sources/SidebarWorkspaceRenderItem.swift
  • Sources/TerminalController+ControlWorkspaceGroupContext.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/ControlWorkspaceGroupCreateParentInferenceTests.swift
  • cmuxTests/SidebarWorkspaceRenderItemStaleAnchorTests.swift
🛑 Comments failed to post (1)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift (1)

61-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mark new @State properties private.

SwiftLint flags ignoredChatSessionRefreshKey, ignoredChatSessionRefreshID, and ignoredChatSessionRefreshTask as non-private SwiftUI state. Since these back internal chat-refresh-ignoring logic (not exposed as bindings elsewhere in this view), they should be private.

🧹 Proposed fix
-    `@State` var ignoredChatSessionRefreshKey: String?
-    `@State` var ignoredChatSessionRefreshID: UUID?
-    `@State` var ignoredChatSessionRefreshTask: Task<[ChatSessionDescriptor]?, Never>?
+    `@State` private var ignoredChatSessionRefreshKey: String?
+    `@State` private var ignoredChatSessionRefreshID: UUID?
+    `@State` private var ignoredChatSessionRefreshTask: Task<[ChatSessionDescriptor]?, Never>?

The discouraged_optional_collection hint on the Task result type appears to be a false positive here (the optional distinguishes "no update produced" from "empty list"); no action needed there.

📝 Committable suggestion

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

    `@State` private var ignoredChatSessionRefreshKey: String?
    `@State` private var ignoredChatSessionRefreshID: UUID?
    `@State` private var ignoredChatSessionRefreshTask: Task<[ChatSessionDescriptor]?, Never>?
🧰 Tools
🪛 SwiftLint (0.65.0)

[Warning] 63-63: Prefer empty collection over optional collection

(discouraged_optional_collection)


[Warning] 61-61: SwiftUI state properties should be private

(private_swiftui_state)


[Warning] 62-62: SwiftUI state properties should be private

(private_swiftui_state)


[Warning] 63-63: SwiftUI state properties should be private

(private_swiftui_state)

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

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceDetailView.swift`
around lines 61 - 63, The SwiftUI state backing the chat-refresh ignore logic is
currently exposed as non-private; update the `WorkspaceDetailView` `@State`
properties `ignoredChatSessionRefreshKey`, `ignoredChatSessionRefreshID`, and
`ignoredChatSessionRefreshTask` to be private. Keep the
`Task<[ChatSessionDescriptor]?, Never>` type unchanged, and only adjust access
control for these internal state members since they are not used as external
bindings.

Source: Linters/SAST tools

@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

Caution

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

⚠️ Outside diff range comments (1)
Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift (1)

331-356: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix the delete count for the last-workspace holdout.

The tabs.count <= 1 branch removes the workspace from the deleted group but never increments closed, despite the comment saying it is still reported as removed. Either increment the count there or update the contract/comment so socket/CLI responses don’t misreport the operation.

Proposed fix if the returned count is “removed from group”
             if model.tabs.count <= 1 {
                 model.assignGroup(workspaceId: tab.id, groupId: nil)
+                closed += 1
                 continue
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift`
around lines 331 - 356, The deleted-group cleanup in WorkspaceGroupCoordinator’s
group-removal flow is undercounting the last-workspace holdout: the `tabs.count
<= 1` branch clears the workspace’s `groupId` but never updates the returned
`closed` count. Fix the `closeWorkspaceForGroupDeletion`-driven loop so this
path either increments `closed` when `model.assignGroup(workspaceId:groupId:)`
detaches the holdout, or adjust the surrounding contract/comment to match the
actual meaning of the returned count. Keep the behavior aligned with the
`closed` accumulator and the final `return closed` so socket/CLI responses stay
accurate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver.swift`:
- Around line 172-177: The parent-group remap in
SidebarWorkspaceReorderDropResolver should not apply to normal workspace drops
on nested folder headers, since it can override the intended target group.
Update the target-resolution logic around the group-header branch and the
subsequent target.groupId handling so that the parentGroupId fallback is only
used for group-anchor sibling reorders, while regular workspace drops keep the
original target.groupId. Use the existing symbols target.isGroupHeader,
target.groupId, groupsById, and parentGroupId to locate and constrain this
branch.
- Around line 143-148: The dragged group check in
SidebarWorkspaceReorderDropResolver should also reject descendant targets, not
just the dragged group itself. Update the candidate validation inside the
draggedGroup branch to walk candidate.groupId’s parentGroupId chain and return
nil if it ever matches draggedGroup.id, preventing explicitGroupId from
resolving back into the dragged subtree.

In
`@Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceGroupCreationPlacementRegressionTests.swift`:
- Line 9: `makeWorld()` is being called through a throwaway
`WorkspaceCoordinatorTests()` instance even though it does not use instance
state. Update `WorkspaceCoordinatorTests.makeWorld` to be `static` (or move it
into a shared test-support helper), then change
`WorkspaceGroupCreationPlacementRegressionTests` and any other callers to use
the new static/helper entrypoint so they no longer need to instantiate the test
suite just to access it.

---

Outside diff comments:
In
`@Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift`:
- Around line 331-356: The deleted-group cleanup in WorkspaceGroupCoordinator’s
group-removal flow is undercounting the last-workspace holdout: the `tabs.count
<= 1` branch clears the workspace’s `groupId` but never updates the returned
`closed` count. Fix the `closeWorkspaceForGroupDeletion`-driven loop so this
path either increments `closed` when `model.assignGroup(workspaceId:groupId:)`
detaches the holdout, or adjust the surrounding contract/comment to match the
actual meaning of the returned count. Keep the behavior aligned with the
`closed` accumulator and the final `return closed` so socket/CLI responses stay
accurate.
🪄 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: 9adb2b90-f068-4903-87dc-dc7d14d79bec

📥 Commits

Reviewing files that changed from the base of the PR and between 36732c8 and 9e329f4.

📒 Files selected for processing (15)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileWorkspaceListItem.swift
  • Packages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileWorkspaceListItemTests.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver+GroupLegalRange.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver.swift
  • Packages/macOS/CmuxFoundation/Tests/CmuxFoundationTests/SidebarDropPlannerTests.swift
  • Packages/macOS/CmuxWorkspaces/Sources/CmuxWorkspaces/Coordinators/WorkspaceGroupCoordinator.swift
  • Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceCoordinatorTests.swift
  • Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceGroupCreationPlacementRegressionTests.swift
  • Sources/ContentView.swift
  • Sources/TabManager.swift
  • Sources/TerminalController+ControlWorkspaceGroupContext.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/ControlWorkspaceGroupCreateParentInferenceTests.swift
  • cmuxTests/SessionWorkspaceGroupSnapshotParentChainTests.swift

struct WorkspaceGroupCreationPlacementRegressionTests {
@Test
func emptyNestedWorkspaceGroupPreservesParentTopLevelPosition() throws {
let (model, host, groups, _) = WorkspaceCoordinatorTests().makeWorld()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Instantiating WorkspaceCoordinatorTests() just to reach makeWorld().

makeWorld() doesn't touch self state; consider making it static on WorkspaceCoordinatorTests (or extracting it to a shared test-support helper) so callers don't need to construct a throwaway instance of the suite.

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

In
`@Packages/macOS/CmuxWorkspaces/Tests/CmuxWorkspacesTests/WorkspaceGroupCreationPlacementRegressionTests.swift`
at line 9, `makeWorld()` is being called through a throwaway
`WorkspaceCoordinatorTests()` instance even though it does not use instance
state. Update `WorkspaceCoordinatorTests.makeWorld` to be `static` (or move it
into a shared test-support helper), then change
`WorkspaceGroupCreationPlacementRegressionTests` and any other callers to use
the new static/helper entrypoint so they no longer need to instantiate the test
suite just to access it.

@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
`@Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver.swift`:
- Around line 191-201: The drop-resolution logic in
SidebarWorkspaceReorderDropResolver should not let ordinary workspace drops fall
back to the parent folder when reordering inside a nested child group. Update
the branch that returns from the parentGroupId fallback so it only applies to
group-anchor sibling reorders, or requires an explicit outdent condition, and
keep workspace drops scoped to the current child group. Use the existing symbols
nextIsSameGroup, parentGroupId, groupIsInSubtree, and context.nextTarget to
locate and guard the fallback path.

In
`@Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver`+GroupLegalRange.swift:
- Around line 170-186: The directChildGroupId helper currently keeps walking
past the case where workspace.groupId already equals rootGroupId, which adds
unnecessary ancestor traversal and obscures the intended “child subgroup of
root” check. Update directChildGroupId(containing:rootGroupId:groupsById:) to
early-return nil when the workspace is directly in rootGroupId, while preserving
the existing loop for deeper nesting and the visited-cycle guard.
🪄 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: 56f398c1-4b24-4385-b137-32f51fba3304

📥 Commits

Reviewing files that changed from the base of the PR and between 0ff67a9 and b04a566.

📒 Files selected for processing (8)
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver+GroupLegalRange.swift
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver.swift
  • Packages/macOS/CmuxFoundation/Tests/CmuxFoundationTests/SidebarDropLegalRangeTests.swift
  • Packages/macOS/CmuxFoundation/Tests/CmuxFoundationTests/SidebarDropNestedBoundaryTests.swift
  • Sources/ContentView.swift
  • Sources/SidebarDropIndicatorRowScopeResolver.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/SidebarDropIndicatorRowScopeResolverTests.swift

Comment on lines +170 to +186
private func directChildGroupId(
containing workspace: SidebarWorkspaceReorderWorkspaceSnapshot,
rootGroupId: UUID,
groupsById: [UUID: SidebarWorkspaceReorderGroupSnapshot]
) -> UUID? {
guard var currentGroupId = workspace.groupId else { return nil }
var visited: Set<UUID> = []
while let group = groupsById[currentGroupId],
let parentGroupId = group.parentGroupId {
guard visited.insert(currentGroupId).inserted else { return nil }
if parentGroupId == rootGroupId {
return currentGroupId
}
currentGroupId = parentGroupId
}
return nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider an early exit when the workspace is a direct member of rootGroupId itself.

When workspace.groupId == rootGroupId, the loop doesn't stop there — it climbs into rootGroupId's own ancestors (outside the subtree) before eventually returning nil. The final result is correct (given the acyclic-tree invariant enforced elsewhere), but the extra climbing is wasted work and makes the intent ("is this workspace nested under a child subgroup of root?") less obvious.

♻️ Proposed early-exit
 private func directChildGroupId(
     containing workspace: SidebarWorkspaceReorderWorkspaceSnapshot,
     rootGroupId: UUID,
     groupsById: [UUID: SidebarWorkspaceReorderGroupSnapshot]
 ) -> UUID? {
     guard var currentGroupId = workspace.groupId else { return nil }
+    guard currentGroupId != rootGroupId else { return nil }
     var visited: Set<UUID> = []
     while let group = groupsById[currentGroupId],
           let parentGroupId = group.parentGroupId {
📝 Committable suggestion

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

Suggested change
private func directChildGroupId(
containing workspace: SidebarWorkspaceReorderWorkspaceSnapshot,
rootGroupId: UUID,
groupsById: [UUID: SidebarWorkspaceReorderGroupSnapshot]
) -> UUID? {
guard var currentGroupId = workspace.groupId else { return nil }
var visited: Set<UUID> = []
while let group = groupsById[currentGroupId],
let parentGroupId = group.parentGroupId {
guard visited.insert(currentGroupId).inserted else { return nil }
if parentGroupId == rootGroupId {
return currentGroupId
}
currentGroupId = parentGroupId
}
return nil
}
private func directChildGroupId(
containing workspace: SidebarWorkspaceReorderWorkspaceSnapshot,
rootGroupId: UUID,
groupsById: [UUID: SidebarWorkspaceReorderGroupSnapshot]
) -> UUID? {
guard var currentGroupId = workspace.groupId else { return nil }
guard currentGroupId != rootGroupId else { return nil }
var visited: Set<UUID> = []
while let group = groupsById[currentGroupId],
let parentGroupId = group.parentGroupId {
guard visited.insert(currentGroupId).inserted else { return nil }
if parentGroupId == rootGroupId {
return currentGroupId
}
currentGroupId = parentGroupId
}
return nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver`+GroupLegalRange.swift
around lines 170 - 186, The directChildGroupId helper currently keeps walking
past the case where workspace.groupId already equals rootGroupId, which adds
unnecessary ancestor traversal and obscures the intended “child subgroup of
root” check. Update directChildGroupId(containing:rootGroupId:groupsById:) to
early-return nil when the workspace is directly in rootGroupId, while preserving
the existing loop for deeper nesting and the visited-cycle guard.

@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
`@Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver.swift`:
- Around line 193-202: The reordering logic in
SidebarWorkspaceReorderDropResolver is only checking the immediate next target’s
groupId, so descendant rows can be misclassified as leaving the current group.
Update the decision in the affected branch to treat any next target that belongs
to the current group’s subtree as still inside that group by using the
structured hierarchy from context and groupsById/parentGroupId, not just direct
equality on nextTarget?.groupId. Keep the return path in the same reorder
resolver flow, but make the inside/outside group check recursive or
ancestry-based so child-folder headers don’t make the divider
rootable/ambiguous.
🪄 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: 9473b0df-70aa-48e2-a869-0f1cbf042860

📥 Commits

Reviewing files that changed from the base of the PR and between b04a566 and 4c6236f.

📒 Files selected for processing (2)
  • Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/SidebarDrop/SidebarWorkspaceReorderDropResolver.swift
  • Packages/macOS/CmuxFoundation/Tests/CmuxFoundationTests/SidebarDropNestedBoundaryTests.swift

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

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

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: nested folders (arbitrary-depth tree) for organizing workspaces in the sidebar — e.g. Hotels > HotelName > terminal

3 participants