cmux NEXT: workspace groups pin in the sidebar and keep their place
Summary
Pin/Unpin Group on a sidebar workspace group refused in cmux NEXT, so a group couldn't be pinned in place. Fixes https://github.com/manaflow-ai/cmux/issues/22259.
With the sidebar document active, a group row's ID is its document item ID. That's itm_g_<id> for a group that came from the personal store (migrated, or synced from an older client). workspaceGroup.togglePin caused two failures:
- It looked up
itm_g_<id>as a personal group and found nothing ("No workspace group"). - It sent the personal
<id>back as the sidebar group, which the document reducer doesn't know (UnknownItem).
What changed:
SidebarDocRecords.groupItem(personal:)returns a personal group's document item (itm_g_<id>or the bare ID), andisPinned(group:)is true when that item sits in Pinned.PersonalStore.group(item:)resolves a document group item to its personal group (itm_g_<id>is the daemon'slegacy_group_id), soAppActionContext.personalGroupand every group-header action find the group.SidebarDocStore.activeRecordsholds the active document's records.WorkspaceGroupHandlers.sidebarIDsends intents to the group's document item, and Pin/Unpin reads the current pin from the document, so the toggle goes the right way. It reads the projection the sidebar shows (SidebarDocStore.showsPinned), pending changes included, so a second click before the daemon answers undoes the first. Ungroup, Delete, Close All, the icon picker and next/previous group name the same item.SidebarDocMachines.model(_:)maps a group in Pinned as pinned. It used to map every group withpinned: false.
A pinned group moves to the end of Pinned and stays at that position. Unpin moves it back to its machine's list.
Pinned workspaces, from the same issue:
- Leading pin: the pin is drawn first, at the left edge of a pinned group header and of a workspace row in Pinned (
GroupHeaderRowView,SidebarItemRowView,SidebarItemInfo.leadingPin). - Pin Workspace on a brand-new workspace: this used to do nothing, because the document didn't place the workspace yet. Now the sidebar adopts and pins it in one change. If its adopt ref isn't known yet, the daemon's pin flag is set instead, and
sidebar_doc_syncadopts the workspace into Pinned. Unpin before that clears the flag (PinCommands+Workspaces.swift). - No drop into Pinned: a drag that holds anything not already pinned can't land in Pinned (
SidebarDocRecords.keepingPinned, applied inSidebarDocStore.acceptance). A workspace dragged between two pinned workspaces no longer becomes a pin; Pin is the way in. Pinned items still reorder inside Pinned.
Testing
Added SidebarDocGroupPinTests (CmuxNextAppTests). It covers personal-to-item mapping, item-to-personal mapping, and the pinned state for document records and projection groups.
- Red commit b458c79e52d61: the test references the mapping API, which doesn't exist on the base, so the test target doesn't compile there.
- Running at merge hand-off: CmuxNext
swift teston the fleet at d41defbc3de5 (cmux-ci run, keypr22263-cmuxnext-d41defbc3de5), which includesSidebarDocGroupPinTestsandSidebarDocPinnedDropTests. It had compiled 817 of 933 targets when the label was added. A fleet compile of 8a403b60fa51 failed on a merge defect (group lookups outsideSidebarDocRecords), fixed in 560398bbc38a. Earlier note: Aworkflow_dispatchofcmux-next.ymlon a PR branch skipscmux-next swift testand the Release compile (they run only onfeat-cmux-nextandnext-batch/*), so the dispatched runs on 75d15dc, 814efe0, 526cbad and ba572cc ran only path routing and preflight. The batch queue runs swift test when it validates the stack. - Later commits: 814efe0 moves the lookups out of the app target. 57f3d56 routes Ungroup, Delete, Close All, the icon picker and next/previous group through the same item ID. 526cbad and ba572cc make Pin/Unpin read the pin the sidebar shows, pending changes included (Greptile), and make Close All write the personal group ID (review).
Observed / Not run
- Observed:
python3 scripts/verify-local.py --affected origin/feat-cmux-next --swift-changed origin/feat-cmux-nextpassed 4/4 (Swift syntax, localization defaults, package groups, feature flags). - Observed:
scripts/cmux-next/check-app-size.sh --base ef7a7d84e4ffcpasses. CmuxNextApp has no source-line growth (83121 at the base and at the head) because the lookups live in CmuxNextDaemon and CmuxNextSidebar. The first head grew the app by 4 source lines and failed the size cap. - Not run: local
swift testorswift build. The build hostcmux-lawrence-2didn't resolve over SSH, which is the existing REPAIR.md row. - Not run: no tagged build was dogfooded. Pin/Unpin wasn't driven in a live app.
- Not covered: Unpin of a kept group with no open member. The reducer's
unpinneeds the group's machine, which it reads from its first workspace member.
Changelog
Fixed: Pin Group pins a workspace group in the sidebar, and the group keeps its place in Pinned
Proof
Tagged build 22259-pin-groups-pr-head of d41defbc3de5a40384593f2b7da50b2c4a0988ca, driven through its own CLI socket:
- Right after launch, workspace-1 wasn't placed by the document (row
workspace(<uuid>), no item). I ranpalette.toggleWorkspacePin, the action behind the menu's Pin Workspace, on it. It moved intosec_pinnedon the first call and drew a leading pin. Unpin removed it from Pinned. - Move to New Group "Frontend", then
workspaceGroup.togglePinfour times in a row: the group went Pinned, list, Pinned, list. While pinned, the header drew a leading pin and the machine list showed "No workspaces".
Not driven: a real mouse drag into Pinned (no drag hook in the CLI). SidebarDocPinnedDropTests covers that rule.
Checklist
- Behavior changes have added or updated tests, or Testing says why not
- No new user-facing strings
- Reviewed with a subagent before merge (findings and resolution in the comments)
🤖 Generated with Claude Code