cmux NEXT: workspace groups pin in the sidebar and keep their place

Merged
AU
16d ago
+217-25

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), and isPinned(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's legacy_group_id), so AppActionContext.personalGroup and every group-header action find the group.
  • SidebarDocStore.activeRecords holds the active document's records. WorkspaceGroupHandlers.sidebarID sends 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 with pinned: 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_sync adopts 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 in SidebarDocStore.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 test on the fleet at d41defbc3de5 (cmux-ci run, key pr22263-cmuxnext-d41defbc3de5), which includes SidebarDocGroupPinTests and SidebarDocPinnedDropTests. It had compiled 817 of 933 targets when the label was added. A fleet compile of 8a403b60fa51 failed on a merge defect (group lookups outside SidebarDocRecords), fixed in 560398bbc38a. Earlier note: A workflow_dispatch of cmux-next.yml on a PR branch skips cmux-next swift test and the Release compile (they run only on feat-cmux-next and next-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-next passed 4/4 (Swift syntax, localization defaults, package groups, feature flags).
  • Observed: scripts/cmux-next/check-app-size.sh --base ef7a7d84e4ffc passes. 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 test or swift build. The build host cmux-lawrence-2 didn'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 unpin needs 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 ran palette.toggleWorkspacePin, the action behind the menu's Pin Workspace, on it. It moved into sec_pinned on the first call and drew a leading pin. Unpin removed it from Pinned.
  • Move to New Group "Frontend", then workspaceGroup.togglePin four 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