cmux NEXT sidebar: New Workspace in Section goes to the top by default, like Cmd-N
Leo, 2026-10-22: "OH YEAH LAWRENCE WANTED DEFAULT NEW WORKSPACE AT THE TOP"
Leo, 2026-10-22: "sidebar i know it also pushes all the workspaces below down pretty hard so when you create a workspace at the top it's a huge cascade..."
Leo-ask: ask-856c31b307 (also ask-995a5ef560)
What was already right on base
workspaces.newPlacement already defaults to top. Lawrence set that in a0a00d1c1b98, and his 3254990e335 makes the daemon's create place a new workspace first in a single change. This PR does not touch either.
These paths already follow the setting:
- Cmd-N /
newTab, the sidebar section header's +, and a double-click on empty space outside a group. - Agent and session creates:
AgentHandlers+Pane,ChatsOpenCoordinator,SessionTransferService(+Chats).
Under top, a workspace created from a member of a group goes right below that member, inside the group. That is #22926, Leo's earlier ask.
Insert animation on base:
- The stand-in row and the daemon's row use the same row key (the
WorkspaceKeyfixed at the press), so the view is reused and nothing swaps. - New rows open from zero height while the rows below move on one critically damped spring (
.move: response 0.15, damping 1). There is no stagger and no overshoot. - For the in-group case,
SidebarDocWiring.awaitsPlacementholds the stand-in at its group slot until the app's move is shown.
What was wrong (culprit 3f309bae4cf, 2026-10-14)
New Workspace in Section on the workspace list always sent .bottom(anchor: nil). The new workspace went to the bottom whatever the setting said.
Change
- New Workspace in Section on the workspace list now places by
workspaces.newPlacement, as if the window showed no workspace (WorkspaceSpawn.placesFromCurrent = false):top(the default): first in the list, below Home. The daemon's create already lists it there, so the app sends no second move.bottom: the end, as before.afterCurrent: after the current workspace.
- The setting itself and its other values are unchanged.
Tests
NewWorkspaceInSectionTests(pure): the section rule undertopputs the new workspace first, not in the current workspace's group, and below Home. It sends no second move.bottomkeeps the daemon's place. The fallback istop.CmdNTopInGroupFramesTests(same-tree daemon, every frame the list applies is recorded):- Cmd-N from a group member under the default draws the new row only right below that member, inside the group.
- Cmd-N from a loose workspace draws it only first.
- New Workspace in Section draws it only first. On base this case puts it last, so this test is the red one.
Fleet:
cmux-ci compile-check --appat head a34e7c1de50c6d39e5f07d6517be1f36522c8cd0: passed (job a37cf746cd3df06c592bfd00).- Suites at head on aws-m4pro-9, a worker with an awake display (step 5ac89354e07fe9f677b4560c). Green:
- SidebarInsertMotionTests
- SidebarInsertWhileDisplayAsleepTests
- SidebarNewRowInPlaceTests
- SidebarBackgroundChangeCalmTests
- SidebarPriorityMotionTests
- CmdNTopInGroupFramesTests
- NewWorkspaceInSectionTests
- PendingWorkspaceRowsRedrawTests
- RapidWorkspaceCreateTests
SidebarRowsMoveOnRenderServerTests: all nine new tests pass.- The four reds in that suite fail the same way at base 05d12a8f on the same worker (step 27b5925303aace7533048a38):
anInsertAtTheTopMovesTheRowsBelowOnTheRenderServer,aSecondInsertStacksOnTheFirstMove,aMoveTheDisplaySleepsThroughEndsAtTheSlots,aReloadWithoutMotionMidMoveShowsTheSlotsAtOnce. They insert above the first shown row, which viewport anchoring keeps still, and they don't mark user intent, which calm movement requires. - Red proofs, each with the fixes reverted and the tests kept:
- Default set back to the full spring (step f6dee4b4fd59eb5450683e5a).
- First review fixes reverted (step 391fb54a585436e2edeb9941).
- Second review fixes reverted (step d4bd6b11e566f47568aeddec).
- Third review fixes reverted (step f23b4c66c990abf04d973d20):
- In an insert plus close, the closed row has no size spring under snap, closes only part of its height under the nudge, and closes on
.tabOpenunder quick. - An offscreen new row is still opening after the display's sleep and after a reload without motion, and it opens again when scrolled to.
- In an insert plus close, the closed row has no size spring under snap, closes only part of its height under the nudge, and closes on
New row motion: versions behind sidebar.insertMotion (ask-856c31b307)
Leo, 2026-10-22: "when you create a workspace at the top it's a huge cascade"
With top as the default, every new workspace opens at the top of the list, so every row on screen slid down one row height on .move. This PR adds a Debug Settings choice, sidebar.insertMotion (Sidebar > New row motion), following the sidebar.allChats.design pattern (SidebarInsertMotion.tunable). It applies only to changes that insert a workspace row. Other row moves (reorder, close, collapse) are unchanged.
| Version | Rows below | New row |
|---|---|---|
| nudge (default) | travel at most sidebar.insertMotion.nudge (6 pt) on .tabOpen (critically damped, response 0.1) | grows only its last 6 pt and fades in, so its bottom meets the row below in every frame |
| quick | whole distance on .tabOpen (about 130 ms to rest, no overshoot) | opens its slot from zero height and fades in |
| snap | in their slots at once; springs left by an earlier move come off | fades in at its slot |
| spring | whole distance on .move (the motion before, #24187) | opens its slot from zero height and fades in |
Pick: nudge. It removes what Leo called the cascade: no row slides a whole row height. It still shows the list making room. Snap reads as a jump: the rows step a full row with nothing tying them to the new row. Quick only shortens the same full-height slide. Under nudge a row below lands within half a point in under 100 ms, against about 140 ms for the full spring over a row height, and the spring is at rest by 150 ms.
Codex xhigh review at 21ce305 (BLOCK), all three fixed in 618df181:
- Two quick inserts stacked two nudges. The cap now bounds what is drawn. The offset the move's springs still carry counts toward it, and one spring from the drawn position replaces them (
SidebarDragSpring.move,inFlight). - Rows listed again took the nudge. A group's expand, a filter or "Show more" lists existing workspaces again, and they got the nudge. The insert motion now applies only when every opening workspace row is new to the model (
opensOnlyNewWorkspaces, againstappliedSections). Expand and collapse move as on base. openingRowsexpired on.movetiming. It now holds the time each open settles, on the spring that open used. A reorder right after a nudge moves the row; it doesn't open it again.
Codex xhigh review at 618df181 (BLOCK; it confirmed the three fixes above). Both findings fixed in 2c21552a:
- P1: a nudge past the new row's height. The tunable allows up to 40 pt. With a nudge past the row's height, a row below that an earlier insert still moved started above the new row and crossed it. The travel now stops at the height of the shortest run of new rows (
SidebarInsertMotion.travel(_:within:),shortestRun), so a row below never starts above the new rows. - P2: rows were marked as opening when nothing opens. Snap and a zero nudge marked every inserted row as opening. A row that a scroll later brought into view then opened from zero height on
.move, whatever the chosen motion. Now:- Snap and a zero nudge mark nothing.
openingRowsholds aSidebarRowOpening(until, motion, travel).RowMotion.placeopens a late row with the insert's start and spring.
Codex xhigh review at 2c21552a (BLOCK; it confirmed the five fixes above). Both findings fixed in a34e7c1d:
-
Insert plus close in one change. The closing row took the insert's cap and spring. The new row motion now applies only to a change that only adds rows (
onlyAdds: every old row is still listed, in order). Any other change moves every row as on base. -
The display-sleep settle left openings armed. I fixed the whole class, not only this path.
settleOpenings()is the one way an open ends early, and every path that places rows without motion calls it:- an apply that doesn't animate (which covers calm reloads, drops and space switches);
- Reduce Motion;
- a list outside a window;
- the display's sleep settle;
- the unanimated branches of
RowMotion.run, which the group fold path also uses.
An animated apply also drops openings of rows it no longer lists. Nothing else this PR adds holds state between applies:
appliedSectionsis rewritten on every apply, and the springs are taken off on every unanimated path, as on base.
Rules kept:
- Reduce Motion: no slide in any version. That branch of
RowMotion.runis unchanged. - Colours and glass: unchanged.
- No other default changes.
ui.animations.newWorkspacestays as it is. - No new per-frame main-thread work. The frames still ride render-server additive springs (
SidebarDragSpring.move). The cap is computed once per moved view when its spring is added: O(rows moved) per insert, not per frame.
Stop condition and cleanup. No new timer, observer or loop. Each spring ends on its own settling time (springAnimation duration = settlingTime(within: 0.002)). The move's RowMotionEnd runs once both legs end, and trackMove(token, settleAtSleep:) ends it at once if the display sleeps. SidebarDragSpring.stop removes the batch springs on a change without motion or Reduce Motion. Snap calls stop itself before it moves.
Tests:
SidebarInsertMotionTests(pure, 9 tests):- The default is nudge (6 pt), and both tunables are registered.
- A row below a top insert starts the nudge above its slot, not 28 pt above it.
- The new row's bottom meets the row-below start, and a workspace with its tab row opens as one slot.
- A group line's end travels at most the nudge.
- Snap starts every row at its slot.
- Quick and spring keep the whole distance.
- A row unfolding under a header keeps its start.
- The chosen spring is critically damped, lands within half a point in under 100 ms, and is at rest by 150 ms.
SidebarRowsMoveOnRenderServerTests.aTopInsertNudgesTheRowsBelowByAtMostTheNudge(window on screen): after a top insert into 30 rows, every additive spring on every row travels at most the nudge and lasts under 150 ms.
Captures: private, taken on capture mini cmux-mac-mini-cua-5, build job 216048236c59b4472b33925d, File > New Workspace with 23 rows below. They are not posted here (private captures rule).
No capture: the captures of this change (build job 216048236c59, File > New Workspace with 23 rows below) are kept private, as the line above says. The placement and motion are held by the fleet GUI suites NewWorkspaceInSectionTests, CmdNTopInGroupFramesTests, SidebarInsertMotionTests and SidebarRowsMoveOnRenderServerTests, all passing at f8f73f4e.
- The recorder drops 75ā160 ms of frames when a workspace is created. Every version's motion is shorter than that, so the strips can't tell the versions apart. They do show:
- quick and spring: the rows are 2ā4 pt short of their slots in the first frame after the gap.
- nudge and snap: the rows are already in their slots.
- No overshoot or flicker in any version.
- In every version, the old spring included, the new top slot stays empty for about 130ā170 ms before the row shows whole. That comes from row creation, not from this motion. It is noted, not changed here.
- Inserts made over the socket (
action.run) snap in every version. That is the calm-reload rule (only changes the user asked for animate), so captures must go through the menu.
Not done
- Group + and the empty-area double-click inside a group still use
workspaceGroups.newWorkspacePlacement, which defaults toend(Lawrence's group config). Leo did not name that default, so it is unchanged here.
Changelog: New Workspace in Section now follows the New Workspace placement setting (top by default) instead of always adding at the bottom. A new workspace at the top no longer slides every row below it down a full row.
š¤ Generated with Claude Code
Agent session: cosmic-gray