Skip to content

cmux-next: TabDragSession, glass ghost, one command per drop outcome - #15555

Merged
lawrencecchen merged 2 commits into
feat-cmux-nextfrom
feat-cmux-next-tabdrag
Sep 29, 2026
Merged

lawrencecchen merged 2 commits into
feat-cmux-nextfrom
feat-cmux-next-tabdrag

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Tab drag session for cmux-next (plans/cmux-next/REWRITE.md "Tab drag"): one drag, every destination.

What changes

  • TabDragSession (CmuxNextApp/Drag) replaces the first cut. One borderless non-activating panel carries the ghost across every window and outside them. Over a strip it folds into an inline tab, and the strip opens a gap. Everywhere else it unfolds into a Liquid Glass card with a live terminal or browser snapshot. The ghost follows the pointer with no lag. Every discontinuity springs: card to tab, slot change, spring back, drop.
  • The session asks every TabDropTargetProviding in every window by screen point, in this order: sidebar, strips, layout. The sidebar and the layout get adapters (TabDropTargets.swift). A pane center maps to "join that pane's strip".
  • Each outcome commits ONE daemon command with a client transaction and an optimistic patch: move-tab, tab-to-new-split, tab-to-new-column, tab-to-new-workspace (placed at the sidebar slot or group), move-tab-to-workspace. Release outside every window tears off: a new workspace opens in a new window whose strip lands under the pointer, and WindowManager persists the window. A drag that carries the whole workspace moves its window instead. Escape, no target, or a rejected command springs the tab back.
  • Whole-group drags use the tab-groups-v1 group moves.
  • TabDragLifecycle makes sure every drag ends exactly once. It calls restoreDetachedTab/restoreDetachedGroup unless the transaction settled.
  • Auto-scroll: strips and the sidebar already scroll themselves. The new LayoutRootView.autoscrollTabDrag scrolls niri columns near the edges. This is a small additive API in CmuxNextLayout.
  • The display link runs only while the ghost animates or columns auto-scroll, and stops when the drag ends. Measured idle CPU is 0.0%.

Fixes

  • Fallback split on the left or top edge failed with swap-pane: unknown pane/target on daemons without tab-drag-v1. Reproduced against a live cmux-tui: dragging a pane's only tab onto its own left edge moves the tab into the new pane, the source pane closes, and the swap targets a pane that no longer exists. The resolver now refuses that no-op drop. The fallback swaps only when the target pane still exists, and uses split {tab} when the daemon honors it.
  • Same-pane move-tab sent the final index, but the daemon expects pre-move insertion coordinates. Moving a tab one slot right did nothing, and longer moves landed one slot short. TabMoveIndex converts the index.

Verification

  • swift build --build-tests passes. swift test passes for the whole package. The new TabDragTests cover outcome resolution, cancel and restore invariants, transaction settle, ghost motion, index conversion, and tear-off geometry. check-no-godfiles.sh passes.
  • Fleet build tabdrag-v1 at d2e6984 was launched locally with a clean environment and CMUX_NEXT_NO_ACTIVATE=1. Through the control socket, tab move-right by one slot now moves the tab; before this fix it was a no-op. With 5 tabs and 2 panes, the app idles at 0.0% CPU and 117 MB RSS.
  • UNVERIFIED: the drag itself (ghost, drop targets, tear-off, spring-load, auto-scroll). The app ignored mouse events posted to its pid while inactive, so a background click did not reach the strip. cmux-cua onboarding is not complete, and a foreground drag would take over the user's display. No recording or mid-drag screenshot was taken.

Known limits

  • Dropping in front of the first column is refused: no daemon command can express it.
  • A strip drop that changes group membership sends a second, group command.
  • With the current daemon (no tab-drag-v1), a new split or column first spawns a PTY, moves the tab in, and then closes that PTY.

Changelog

none

🤖 Generated with Claude Code


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

Replaces the first tab-drag cut with a full TabDragSession: one ghost panel follows the pointer across every window and outside them, and every drop outcome commits exactly one daemon command.

Drag session

  • The ghost folds into an inline tab over strips (opening a gap) and unfolds into a Liquid Glass preview card with a live thumbnail elsewhere.
  • Every surface in every window is hit-tested by screen point, in priority order: sidebar, strips, layout; whole-group drags use tab-groups-v1 moves, and releases outside every window tear off into a new window under the pointer, or move the source window when the drag carries the whole workspace.
  • Each outcome sends one command with a client transaction and optimistic patch; TabDragLifecycle guarantees every drag ends exactly once and restores the source tab unless the transaction settled.
  • The new LayoutRootView.autoscrollTabDrag scrolls niri columns near the edges; the display link runs only while the ghost animates or a screen auto-scrolls.

Fixes

  • Same-pane move-tab now converts final indices to the daemon's pre-move insertion coordinates (a one-slot move right was a no-op, longer moves landed a slot short).
  • Fallback split on left/top edges no longer swaps with a pane that moving the pane's only tab closed (unknown pane/target); the resolver also refuses splitting the source pane with its last tab.

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

Review in cubic

lawrencecchen and others added 2 commits September 29, 2026 01:48
Pure, tested logic for the App's TabDragSession: TabDragResolver maps
drop-target proposals to one outcome (and rejects no-op or layout-breaking
drops such as splitting a pane with its only tab), TabDragLifecycle
guarantees every drag ends exactly once and restores the source tab unless
its transaction settled, TabDragGhostMotion tracks the pointer with zero
lag while springing every discontinuity, TabMoveIndex converts final
indices to move-tab's pre-move insertion coordinates.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The drag session owns one borderless panel whose ghost follows the pointer
across every window and outside them, folds into an inline tab over strips
and unfolds into a Liquid Glass preview card (live terminal/browser
snapshot) elsewhere. It asks every TabDropTargetProviding in every window
(sidebar, strips, layout adapters) by screen point, commits one daemon
command per outcome with a client transaction and optimistic patch, springs
back on Escape, tears off into a new window under the pointer (or moves the
window when the drag carries the whole workspace), and handles whole-group
drags. A display link runs only while the ghost animates or columns
auto-scroll (new LayoutRootView.autoscrollTabDrag).

Fixes: same-pane move-tab sent final indices where the daemon expects
pre-move coordinates (a one-slot move right was a no-op); the fallback split
for left/top edges swapped with a pane that moving its last tab had closed
(swap-pane: unknown pane/target). The swap now runs only when the target
survives, and the resolver refuses a split of the source pane by its only tab.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 29, 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: 1645735e-3984-471d-88b8-7ff907a358bf

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

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.

@lawrencecchen
lawrencecchen merged commit a367ef4 into feat-cmux-next Sep 29, 2026
21 of 22 checks passed
@lawrencecchen
lawrencecchen deleted the feat-cmux-next-tabdrag branch September 29, 2026 09:40
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