Skip to content

sidebar toggle: panes glide with the sidebar on cmd b - #18840

Open
lucasr1b wants to merge 16 commits into
sidebar-animationsfrom
sidebar-toggle-glide
Open

lucasr1b wants to merge 16 commits into
sidebar-animationsfrom
sidebar-toggle-glide

Conversation

@lucasr1b

@lucasr1b lucasr1b commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

summary

stacked on #18334 (organize/drag). this is the cmd b sidebar toggle glide, split out as lawrence suggested.

when the sidebar hides or shows, every pane now glides to its own docked edges on the same spring as the sidebar, instead of the old rigid slide:

  • the titlebar title and the minimal mode tab bar inset glide with the slide
  • each pane's whole subtree (terminals, browser, file views) moves and clips to its own rect, so nothing leaks into the next pane. divider strips stand in for the real dividers during the slide
  • each tab bar is laid out once at its pane's width for the whole slide, so tabs and their scroll offset hold still and fade at the pane's moving edge, the same way bonsplit fades them at rest
  • the action lane (new terminal, browser, split buttons) rides each pane's trailing edge, still and flush, on both hide and show
  • one layout commit and one terminal resize per toggle

needs the bonsplit change in manaflow-ai/bonsplit280 (vendor/bonsplit points at it).

testing

  • tested by hand by lucas on a tagged dev build: hide and show, several panes, several tabs, long titles
  • labs/GlideLab pixel checks (edges.py plus a trailing flush check) on the tabs, 3, mix, 2x2, browser, file, 20-80, nest-v, mix4 layouts and light mode, both directions, normal and 4x slow speed

known leftovers

  • a one frame snap when you toggle while a split is still animating
  • the moving button picture can look a bit darker on translucent tab bar themes
  • lab only: a 75 pt docked pane is narrower than its toolbar, and a 0.7 to 1 pt landing offset now and then (both same as before this pr)
  • rprompt text is clipped during the glide (terminals resize once, at the end)
  • still to check by hand: fullscreen, reduce motion, two displays, dock/float, peek

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Reworks the cmd+B sidebar toggle so each pane glides to its own docked edges on the same spring as the sidebar, replacing the previous rigid slide of the whole content root. The titlebar title, minimal-mode tab bar inset, and each pane's trailing action lane glide in step, content is clipped to each pane's rect so nothing leaks into neighbors, and tab bars are laid out once at the pane's width so tabs and their scroll offset hold still and fade at the moving edge. The live right sidebar is held still against the moving root instead of snapshotted, so file views don't flicker brighter mid-slide. The browser toolbar holds its compact state through a slide and fades the change on landing, so its tools don't pop in or out mid-motion. One layout commit and one terminal resize occur per toggle, and closing the window mid-slide drops the slide cleanly instead of leaving a stale layout observer for the app's lifetime. The workspace card now runs flush to the window's bottom and trailing edges, so no dead strip appears under a browser page; the compact toolbar's import chip shows as a square icon, and the omnibar clips its text rather than ellipsizing, so neither shifts at the slide's landing.

Fixes

  • Press-time picture capture is about halved by skipping the unused tab bar strip picture and caching class name checks.
  • Each glide on a layer now gets its own animation key, fixing divider stand-in strips that started off-position and only caught up at landing.
  • The real dividers are hidden with an empty mask for the slide, since AppKit redraws them during a show.
  • Requires the bonsplit change in manaflow-ai/bonsplit280 (vendor/bonsplit points to it).

Known leftovers

  • One-frame snap when toggling while a split is still animating.
  • Moving button picture can look darker on translucent tab bar themes.
  • rprompt text is clipped during the glide (terminals resize once at the end).
  • Fullscreen, reduce motion, two displays, dock/float, and peek still need manual testing.

Tested by hand on a dev build and with GlideLab pixel checks across multiple layouts, speeds, and light mode.

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

View guided diff Turn on auto-fix


Note

Medium Risk
Large Core Animation and layout-timing changes across window chrome, Bonsplit panes, and portal geometry; depends on vendor Bonsplit slide-width APIs and has known edge cases (mid-split toggle, fullscreen/peek not fully verified).

Overview
Replaces the cmd+B sidebar toggle rigid content-root slide with per-pane motion on the same spring: panes, portal-hosted terminals/browsers, split dividers, tab action lanes, and titlebar title (via SidebarSlideGlideHost) each glide/clip between hidden and docked layouts using new capture/layout/glide machinery (SidebarSlidePaneLayout, SidebarSlidePaneChrome, SidebarSlidePaneGlide, SidebarToggleSlideSession).

SidebarToggleAnimator now captures docked geometry at hide press, rebuilds mid-slide on split resize (restart), syncs minimal-mode tab bar inset on dock commits, exposes isSliding / slideDidLand so UI (e.g. browser toolbar compact state, omnibar clipping) holds steady until the slide lands. Right sidebar column stays live (inverse spring + masks) instead of a snapshot overlay.

Chrome polish: workspace card drops bottom/trailing pane insets and bottom-leading rounding; traffic-light tab inset respects docked vs hidden; browser import chip goes icon-only in compact mode; portal registry types loosened for slide introspection (entriesByWebViewId, Entry). DEBUG probes/timings extended; unit tests cover glide math, action-lane width, and slide restart.

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

lucasr1b and others added 7 commits October 9, 2026 15:41
…he slide

the title row rode the content root's slide with the hidden layout's
padding, so a show carried it a sidebar width right and snapped it back at
the landing, and a hide threw it right at the keypress. it now sits in its
own layer and glides from its hidden rest to its docked rest on the same
spring, render server only. its padding and the titlebar border follow the
docked layout flag, so they change in the same commit as the layout.

minimal mode's tab bar inset now changes inside the atomic layout commits,
and the first pane's tabs ride the slide as a picture that glides by the
inset over plain tab bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
the slide carries the full-width layout past the content's trailing edge,
so the action buttons of panes on that edge slid out under the right
sidebar and popped back at a show's landing, and vanished at a hide's press.
they rest at the same x in both layouts, so the slide now shows a still
picture of them in place, taken at the press, and drops it in the landing
commit.

also fixes the pane lookup the minimal tab row picture relies on (split
leaves are plain hosting views in a split slot, and the tab bar background
shares the tab hit region protocol), keeps pictures through display passes,
and moves the slide session into its own file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bonsplit keeps split ratios, so the hidden layout's panes are wider in
proportion and one rigid slide carried every edge past the leading one too
far: a show threw an inner divider, the pane right of it and the left pane's
buttons about 120 pt past their place for a 50/50 split and snapped them
back at the landing, a hide the mirror.

now each split slot (and a lone pane) moves by its own leading edge
relative to the slot around it and clips at its own trailing edge, the
portal's terminal and browser views follow their pane, divider strips in
each split's colour ride the gaps (the portal's divider overlay is faded
out meanwhile), and each pane's action buttons ride its trailing edge as a
picture. all of it is spring animations in the same transaction, render
server only. the docked rects come from the hide's press, from the last
hide over the same hidden layout, or failing both from bonsplit's rule.

adds a DEBUG per-frame edge log (CMUX_SIDEBAR_SLIDE_EDGES) for checking
the motion without screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ays flush with its pane

per-pane motion now covers everything drawn for a pane, by construction:

- the portals' views (terminals, browser pages, in or out of window glass)
  come from the portals' own entries and follow the pane their anchor sits
  in, glided and clipped
- each pane's trailing chrome is pictured from the layout that is live for
  the slide (a hide's right after its commit) and rides the pane's trailing
  edge as a sublayer of the pane's own clip, so it always sits flush and
  whole inside the pane and can never leave it. the tab bar's part is
  bonsplit's action lane by its fixed width; other bars (browser toolbar,
  path bar, composer) are cut at a seam between rows alike all the way
  across, only the bar at the strip's edge, never more than half the pane,
  never in a web view
- overlays inside a portal view keep their anchoring by geometry (scroller,
  badges trailing, cards centred), drawn ones (find bars, rings, borders)
  are pictured and cut the same way inside the view's clip
- bonsplit's own divider drawing is blanked for the slide (the strips stand
  in), and overlays painting at every pane's rect at once (portal divider,
  pane swap, drop zones, tmux rings, cloud failure card) are hidden with an
  empty mask, which survives the portal re-adding them
- minimal mode's leading tabs ride inside the leading pane's clip
- a bonsplit split resize mid-slide (a split, a close) lands the slide at
  once on the new layout

the debug edge log now includes browser pages and the trailing pictures,
and a layer dump (CMUX_SIDEBAR_SLIDE_LAYER_DUMP) for finding strays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tab chips cut by the action lane picture read as broken. the tab bars now
lay themselves out at the pane's visible width every frame (bonsplit's new
tab bar width hook, driven by a display link on the slide's own clock), so
tabs truncate for real with their ellipsis and close button and the action
lane is live, whole and flush with the pane's moving trailing edge. no tab
row pictures remain. the slide's clock now starts at core animation's actual
begin time, so the per-frame width follows the spring frame for frame.

a split or close mid-slide now rebuilds the slide for the new layout and
goes on from where it is with its speed, instead of landing at once.

points vendor/bonsplit at a local commit (needs its own bonsplit pr).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
on a hide the panes' action buttons bounced: the tab bars were laid out on the
main thread a frame or more behind the pane edges core animation moves (about
25 ms a frame in debug, and the first frame of a hide showed the full hidden
width before any slide width applied).

the lane is now a picture of bonsplit's own lane, at the width bonsplit
reports, riding the pane's trailing edge on the same spring as the edge, with
bonsplit's real lane hidden for the slide. the tabs still lay out live, but at
the narrower of this frame's width and the width 34 ms on, so a missed frame
only shows a little bar surface before the lane, never tabs under it. the first
frame lays out at the slide's start pose, and every width applies in the same
transaction (needsLayout before layoutSubtreeIfNeeded).

points vendor/bonsplit at local 5b3623d.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lucas saw a black gap and a stale title fragment in a tab bar mid-slide. the
tabs were laid out on the main thread every frame, a frame or more behind the
pane edges core animation moves, so they jumped and left bare bar before the
lane. the lane picture was also cut from the whole bar: the ground behind it
(the black) and the fading end of a long title under the lane (the fragment).

now each tab bar is laid out once at its pane's own width for the whole slide,
so the tabs and their scroll offset never move, and masks riding each pane's
trailing edge on the slide's spring fade the tabs and the selected tab's
indicator exactly where bonsplit fades them at rest. the lane picture is the
lane alone, on clear: its buttons and the bottom line under it. no per-frame
tab layout or display link is left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: cc26d6e2-e46c-4ce4-a7f5-ad12ecd0817a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Unclear: macos / macOS compile admission failed: no known signature; failed step: Compile app-host test product.

CI failed on c51366e99a (run 38026752780 attempt 1): 1 unknown.

Job Verdict Why
macos / macOS compile admission unknown no known signature; failed step: Compile app-host test product

Not re-run automatically: macos / macOS compile admission is not a machine failure.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Dogfood tours of c51366e9

sidebar-and-chrome-tour at c51366e9: not run

skipped: CI left no app build for this head (its compile failed or was cancelled)

Tours are picked by the paths globs in dogfood/scenarios/*.json; a Dogfood-tours: a, b line in the description picks them instead (none turns this off). Look at every frame before merging: a green tour only means no step failed.

the layout observer was only removed on teardown, so a window closed
mid-slide left it registered for the life of the app.
…ot, so files don't flicker brighter mid-slide
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

CI fast guards failed on c51366e99a (https://github.com/manaflow-ai/cmux/actions/runs/38026752780). It does not block the merge; a red guard merged into main breaks it for every open PR.
The log named no failed step; see the run.

Agents: python3 scripts/ci/guard_attribution.py fix applies the mechanical fixes locally. This comment is updated in place on each push.

…class name checks

each press pictured every tab bar strip with cacheDisplay and then threw
it away, since tab bars only use the lane picture. skipping it about halves
the press-time capture with 6 panes (45-65 ms down to 15-33 ms in debug).
class name checks run on every view in the window at the press, so their
answers are kept per class. debug-only timing laps for the press too.
a browser pane crosses the toolbar's 420 pt compact width when the sidebar
toggles, so the pen and dev tools buttons popped in or out mid-slide. the
toolbar now holds its compact state while a slide runs and fades the change
in once it lands.
a horizontal divider's stand-in strip both moves and widens, but every
glide was added under the same key, so the width animation replaced the
move. the strip's left end started about 120 pt right of the pane edge and
only caught up at the landing, leaving the border above a lower pane cut
short mid-slide. debug-only CMUX_SIDEBAR_SLIDE_STRIP_DEBUG draws the strips
red to check them.
appkit draws a split view's divider into its own content sublayer and
redraws it during a show, which put back the contents the slide had
blanked. the real divider then rode with the content root next to its
stand-in strip, a second vertical line ending about 120 pt off. an empty
mask, which appkit leaves alone, keeps it hidden for the whole slide.
…r text hold still through the slide

- the workspace card's 8pt bottom inset showed as a dead strip under a browser page
- compact toolbars show the import chip as a square icon, so it never truncates to "I..."
- the omnibar clips its text and placeholder instead of ellipsizing or wrapping, so a
  slide's landing doesn't snap "Search or enter URL", focused or not
with the right sidebar closed the 8pt trailing inset showed as a strip beside a browser page
@lucasr1b
lucasr1b marked this pull request as ready for review October 10, 2026 05:20

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 4 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c51366e. Configure here.

self.isPeekPresenting = isPeekPresenting
self.dockedLayoutWillCommit = dockedLayoutWillCommit
self.tabBarInsetDelta = tabBarInsetDelta
self.splitButtonCount = splitButtonCount

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reinstall leaves slide flag stuck

Medium Severity

A re-install drops the running session and resets the machine, but never calls markSliding(false). isSliding can stay true and slideDidLand never posts, so browser chrome that captured heldChromeCompact stays frozen after the view reappears mid-toggle.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c51366e. Configure here.

guard sliding != Self.slidingAnimators.contains(id) else { return }
if sliding { Self.slidingAnimators.insert(id) } else { Self.slidingAnimators.remove(id) }
Self.isSliding = !Self.slidingAnimators.isEmpty
if !sliding { NotificationCenter.default.post(name: Self.slideDidLand, object: 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.

Land notification is process-global

Medium Severity

markSliding(false) always posts slideDidLand with no window, even when other animators are still in slidingAnimators. Every BrowserPanelView then clears heldChromeCompact, so a second window's landing can pop the first window's toolbar mid-glide.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c51366e. Configure here.

// observer goes with it.
removeSlideAnimations()
markSliding(false)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Closed window leaves machine sliding

Medium Severity

When window() is nil mid-slide, teardown clears the session and isSliding but never lands or resets the machine. machine.slide stays set, so a later press on the same animator retargets a ghost slide instead of starting cleanly.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c51366e. Configure here.

Comment thread Sources/ContentView.swift
backgroundColor: appearance.resolvedChromeBackgroundColor
)
.padding(.leading, sidebarState.isVisible ? width : 0)
.padding(.leading, sidebarState.isVisible && sidebarLayout.docksSidebar ? width : 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Titlebar hairline snaps on hide

Medium Severity

The titlebar hairline inset now follows docksSidebar, which flips at hide start, but this border lives in the window overlay and has no glide. The line jumps to the window's leading edge while the sidebar is still on screen.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c51366e. Configure here.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant