Skip to content

CmuxNext: a tab dropped on a strip lands by the pointer (cx-6ggef) - #20549

Closed
teamleaderleo wants to merge 131 commits into
feat-cmux-nextfrom
leo/strip-drop-pointer
Closed

teamleaderleo wants to merge 131 commits into
feat-cmux-nextfrom
leo/strip-drop-pointer

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes cx-6ggef. On feat-cmux-next, a tab dragged onto the right end of another pane's last tab landed before that tab, and the golden split-drag case move.to-other-pane failed (classic's bonsplit passes it). For an external drop, the strip took the pointer for the middle of a phantom tab as wide as the strip's standard width, so the "after" slot needed the pointer past the tab's right edge plus half a standard width.

The drop now goes to the slot edge nearest the pointer: past a tab's middle means after it, the way bonsplit and Chrome behave and where the ghost card sits. Group drags keep their width-based placement. The change is in TabStripView.resolvePhantom (TabStripView+Phantom.swift). The hit test still uses the base layout, so the gap moving doesn't make the index flicker. Same-strip reordering is untouched (it uses the dragged tab's leading edge in TabStripView+Drag).

scripts/cmux-next/strip-drop-e2e.py (new) splits a three-tab workspace right and drags a left tab, at hand pace, onto the right pane's tab: at 85% of its width (expects after) and at 15% (expects before). It runs twice: once with default titles, once with one-letter titles (renameTab).

Testing

check before after (this head, 6d02cdb)
strip-drop-e2e.py FAIL 2/4 on feat DEV 5bf8644: both 85% drops land before the tab PASS 4/4
golden split-drag.move.to-other-pane, real HID on cua-4 FAIL: w1.p2 = [p1.t1, p2.t1] PASS on 27387ea (same resolvePhantom change, older base)

An intermediate commit also gave the drop math title-fitted widths. Base fe71fdf then removed title fitting, so the merge drops it.

Pre-push: crash ratchet, concurrency, god files (TabStripView did not grow), system-blue all pass.

Fleet receipts on 6d02cdb: check-release-compile.sh (Xcode 26 Release) BUILD SUCCEEDED; run-swift-testing-suites.sh Packages/macOS/CmuxNext 296 passed, 1 failed: NowPlayingCardTests (sidebar now-playing card), whose test process died mid-test with no assertion, as in this PR's macOS 27 report-only run. That is unrelated to the tab strip. PR reds that don't gate: Tour the app (infra), rails (red on feat too).

Proof

Before/after recording of the tab row, from the app's window snapshots (cua-4 DEV builds): A dropped on the right end of TARGET lands as A, TARGET before and TARGET, A after; B at 15% lands first in both. I watched it. The media is published inline at landing.

Changelog

Fixed: A tab dropped on the right half of another pane's tab now lands after it, not before

🤖 Generated with Claude Code

lawrencecchen and others added 30 commits October 13, 2026 21:03
…s its App Attest provider; freeModel.enabled (cx-ds09i)

Relay (cmux-coderouter free_tier.rs): attest_provider on the admin socket,
peer-checked (same user; a Team-signed build: the cmux app that contains the
relay, cmux-link app_caller), production origin only in release apps (staging
and the dev Worker only in DEV builds), one provider and one request at a time.
The relay builds the client data hash from the server challenge, keeps the free
token in memory only (never in status or logs), drops it when the provider
ends, when the setting turns off or an account signs in (also for a lease in
flight), and starts a lease only at the first signed-out data-plane request
(20 s, in its own task). App Attest refuses a second attestation of a key in a
later launch, so a new month takes a fresh key. The account bearer wins.

App: Packages/Shared/CmuxAppAttest (DeviceAttesting, AppleDeviceAttest,
KeychainAttestKeyStore); FreeTierAttestProvider (CmuxNextAgentPane) answers only
purpose cmux.free-tier.v1 over a 32-byte hash on an AcpmuxCheckedLine
connection, one keychain item per bundle id; AppServices+FreeTier follows
freeModel.enabled (default on, MDM lock, agents refused: privacy) and the
sign-in (a restoring session counts as signed in).

Behavior tests: acpmux/tests/router_free_tier.rs (real relay, admin socket,
stand-in app and API; 7 tests, Linux rbx and hosted macOS 38362901804). Red
check of the in-flight race at a3b1066 recorded on cx-ds09i.
scripts/cmux-next/free-tier-attest-e2e.sh: T1/T2 on a real Mac.
Security review hq-84: APPROVE at 8b1c3f8.
…xports reset to the tip before regeneration (cx-ds09i)
…LI (red) (cx-ebm.60.1, cx-ebm.60.2)

The person makes, edits and deletes Chiefs with cmux chief new|list|edit|delete;
a Chief on its own bound connection changes its own picture and only the
fields its self_edit lists; another agent is refused. Red: the daemon has no
chief.list/get/create/update/delete/avatar.* operations yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dit|delete (cx-ebm.60.1, cx-ebm.60.2, cx-ebm.60.3)

Multiple Chiefs per Chief home (Lawrence 2026-10-14: "create new chiefs too
with specific roles, names, ... profile pics (chiefs need to be able to do it
themselves too), personality, system prompt, tools").

Daemon: chief_registry.rs owns chiefs.sqlite3 in the session state
directory. A record has id (= the conversation participant id; agent_mux is
the default Chief, written on first open; new ones agent_<26 base32>),
display_name, role, personality, instructions (system prompt additions),
avatar (PNG/JPEG/GIF/WebP <= 1 MiB, bytes by SHA-256, collected when no
record names them), tools (closed catalog), harnesses, engine, self_edit,
is_default, rev. Deleted ids stay as tombstones. Idempotency ledger per
caller.

cmux.protocol/2: chief.list, chief.get, chief.avatar.get (read);
chief.create, chief.update, chief.delete, chief.avatar.set (mutation).
Callers: the owner (Unix, no link peer, user_local) and the Chiefs (a
connection bound with conversation-bind to a live record's id). A Chief
reads every record and changes only its own self_edit fields (default:
avatar); tools, harnesses, engine, self_edit, create and delete are the
owner's; anything else is chief_self_edit_denied before any write. Pages,
WebSocket, relayed and remote connections and non-Chief agents get
origin.forbidden. Not MCP tools.

CLI: cmux chief list | new --name N [profile] | edit CHIEF [profile]
[--clear-avatar] | delete CHIEF, CHIEF an id or a unique name, --json.

schemas/chief-profile/tool.json: the Chief's self-edit tool (profile) for
the brain (wired in cx-ebm.60.5 after the per-build Chief home lands).

Catalog: 271 operations (84 read, 168 mutation).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…moved AgentTabPaneSource.paneEnvironment) (cx-ds09i)
…nabled on the tip (cx-ds09i; export-settings cmux-ci step 02f78faaf8d0)
…cx-ebm.60.1, cx-ebm.60.2)

Review (correctness subagent, 2026-10-14):
- The cloud catalog already has chief.list/create/update with another
  shape; the app platform merges both catalogs by name with the local op
  winning, so equal names hid the cloud ops. The daemon ops are now
  chief.profile.list/get/create/update/delete/avatar.set/avatar.get; the
  generated app-host types keep the cloud chief.* ops.
- idempotency.conflict names the committed operation; the ledger keeps the
  newest 256 keys per caller.
- harness, model and effort are single-line.
- CLI: "needs a newer daemon" only when the first chief.profile.list call is
  refused as unknown; a value error prints the daemon's reason. The picture
  is checked for 1 MiB before any write; when the picture write fails after
  the create or edit committed, the CLI says what committed. edit with no
  change, and --clear-avatar outside edit, are usage errors.
- The spec states the trust limit (a Chief turn with a shell is the same OS
  user; the self_edit rule guards the model); hardening is cx-ebm.60.9.
- Generated: app-host d.ts/ops/scopes and the AppPlatform scopes copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ext-multichief

# Conflicts:
#	cmux-tui/bindings/cpp/.cmux-resource-api.json
#	cmux-tui/bindings/go/.cmux-resource-api.json
#	cmux-tui/bindings/java/.cmux-resource-api.json
#	cmux-tui/bindings/python/.cmux-resource-api.json
#	cmux-tui/bindings/rust/.cmux-resource-api.json
#	cmux-tui/bindings/typescript/.cmux-resource-api.json
#	cmux-tui/bindings/zig/.cmux-resource-api.json
#	cmux-tui/spec/resource-api-v2.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gate.sh runs split, newterm, split with a slow host, new-tab and cold new-tab proofs on the GUI host and fails on a lost key; typeahead-compare.py counts keys sent by terminal id; creation-hold-proof.py is superseded (named in the gate header)
…testStarter); CmuxNextApp keeps a two-line hook (cx-ds09i; CmuxNextApp size cap)
… the app's life (cx-ds09i; CmuxNextApp size cap)
…ale submodule checkout had moved it back); signed-in fallback true when the services are gone (cx-ds09i move-check)
…plus only the CmuxAppAttest lines (cx-ds09i final check: a stale regeneration had dropped CmuxNextBrowserHistory, CmuxNextCLIInstall, CmuxNextNowPlaying and two CmuxNextCompat test edges)
ae65773 (cx-ds09i; export-settings cmux-ci step 788077f4a088)
…-size baseline takes the tip's 113750 (the tip's own raise covers the hook)
lawrencecchen and others added 22 commits October 14, 2026 04:01
…erves (cx-4bd8m)

Austin's cmux NEXT registry grew to 10.7 GB in 8.5 h under a build without
journal maintenance (auto_vacuum off, 4.47M journal rows). The maintenance
worker's first step converts such a registry with PRAGMA auto_vacuum=
INCREMENTAL; VACUUM; while it holds the registry: on that file every client
of the owner would read "not connected" for minutes after the next update.

Convert only while live data is at most 64 MiB. Above it the conversion is
deferred (one diagnostic per owner); sealing still runs and deletes archived
rows, and SQLite reuses their pages, so the file stops growing once sealing
keeps up. It does not shrink: sealed segments stay in the registry, so giving
the free pages back needs a compaction that does not hold the registry
(follow-up bead). Debug-only overrides for the interval and the bound keep
the behavior test small. Red/green: legacy_registry_maintenance.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Narrow tabs (titles that fit them) still took a right-end drop as before
the tab on the first fix: the drop math lays the strip out without
title widths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…20534)

* test: a local merge commit reuses the fetched same-tree acpmux (red)

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

* fix: a local merge commit reuses the fetched same-tree acpmux

reload.sh on a developer Mac stopped with "no cached acpmux" for any commit
no workflow published (a local integration of feat-cmux-next and PRs), even
though the tree fetch had just put that tree's checksum-verified acpmux in
place. build-acpmux.sh now seeds its commit cache from it when it has every
requested architecture.

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

* chore(next): PR autofix at 7be0d24

- regenerated schemas/defaults/cmux-next.json

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38542356066

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>
* webviews: test that every popup draws one hairline edge

Red: --acpmux-popup-elevation stacks a composer-edge hairline on the one
already in --ui-popup-shadow.

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

* webviews: popups draw one hairline, not two

--acpmux-popup-elevation stacked a 0.5px composer-edge hairline on the
hairline already in --ui-popup-shadow. It now is the shared popup shadow,
so every menu and popover has one edge. Increase Contrast colors that one
edge in the popup's ink, which the composer-edge hairline did before.

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

* chore(next): PR autofix at 1cc6616

- vp fmt webviews/src/agent-session/acpmux/modelPickerLayout.ts

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38520503432

* webviews: test that popups draw no border over the hairline and clear it under borders none

Red: the pages' .ui-popup and the chat find bar draw a border over the
--ui-popup-shadow hairline, and appearance.borders none leaves the
hairline drawn.

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

* webviews: one rim on page popups and the find bar; borders none clears it

The pages' .ui-popup drew a 1px border over the --ui-popup-shadow
hairline; it now draws no border. The chat find bar keeps its border
(its focus cue) and casts --ui-popup-drop, the shadow without the
hairline. appearance.borders none sets --ui-popup-edge transparent.

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

* webviews: format modelPickerLayout.ts (base fmt red)

Keeps cmux-autofix's vp fmt of the base file, which the base merge
replaced with the unformatted copy; react-apps-check fails on it.

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

* webviews: the Channels palette draws one rim; test follows the summary card

Red after the base merge: the new Channels palette (.lg-palette, ledger.css)
drew a 1px border over the --ui-popup-shadow hairline, and the summary
popover became .acpmux-summary-card. The palette now draws no border.

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

* chore(next): PR autofix at 54dd59d

- regenerated schemas/defaults/cmux-next.json

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38543983469

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>
…ggef)

The drop math laid the strip out without the chrome style's title widths,
so a narrow tab read as twice its drawn width and a drop on its right end
landed before it. Both layouts now apply TabTitleWidths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(clears the concurrency red) (#20485)

* fix: clear two feat-cmux-next check reds

tree-publish-v2's stub uploader takes --conflict-exit-code, which the tree
publisher now passes (af8b68e). PaneAnimationsSetting carries the
namespace-type justification its ratcheted sibling settings have.

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

* fix: keep PaneAnimationsSetting internal to CmuxNextSettings

Nothing outside the module uses it, so it needs no namespace-type allow.

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

* chore(next): PR autofix at c2bdb98

- regenerated schemas/defaults/cmux-next.json

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38530638157

* fix: name the owner of ControlWorkspaceInfoCache's publish hop

check-concurrency flags the unowned Task d0a62be added; it is a
one-shot main-actor hop that holds the cache weakly.

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

* chore(next): PR autofix at 7cf07e3

- regenerated schemas/defaults/cmux-next.json

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38544346314

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>
…hover card (#20483)

* test: a notified compact row stays one line; its text reads in the hover card

Leo 2026-10-14: "why is it even 2 lines in the first place?"

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

* fix: compact sidebar rows are one line by default; the notification text moves to the hover card

sidebar.workspaceRow.notification is now off by default (Leo 2026-10-14). #19862
turned it on for classic parity; the setting stays as an opt-in.

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

* chore: regenerate the settings schema hash for the notification default

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

* chore: regenerate the settings schema hash on the merged base

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

* chore: take base's settings exports after the base merge (regenerated next)

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

* chore: regenerate the settings exports on the merged base

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

* chore: refresh the defaults snapshot on the merged base

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

* docs: the hover card template lists the notification fact; comments say where the text shows

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…p-drop-pointer

# Conflicts:
#	Packages/macOS/CmuxNext/Sources/CmuxNextTabs/TabStripView+Layout.swift
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Split plan: no split needed

This PR's real change is 2 files, +196/-3, not 251. The GitHub file list (free tier, Chief registry, backend, webviews, OptChat) is stale: GitHub stops recomputing a conflicting PR's diff. compare feat-cmux-next...6d02cdb8 has merge base 2834e65 and exactly two files:

  • Packages/macOS/CmuxNext/Sources/CmuxNextTabs/TabStripView+Phantom.swift: the resolvePhantom fix (3 lines)
  • scripts/cmux-next/strip-drop-e2e.py: the live check (new, 193 lines)

The free-tier, Chief and settings commits in the list came in through merges of feat-cmux-next and are already on it. None of the two real files overlap #20653 or #21322. The code exists only on feat-cmux-next (main has no CmuxNextTabs), so the slice targets feat-cmux-next, not main.

The only conflict is one line: feat's tab-strip refactors changed contentX to scrollMapping.contentX(...). The rebased change keeps feat's mapping.

Slice: #21626, the same two commits (live check, then fix) on the current feat-cmux-next tip.

Recommendation: close this PR once #21626 lands.

teamleaderleo added a commit that referenced this pull request Oct 18, 2026
…21626)

* test: live check for a drop on another pane's tab (cx-6ggef)

strip-drop-e2e.py splits a three-tab workspace right and drags a left
tab at hand pace onto the right pane's tab at 85% (expects after) and
15% (expects before) of its width, with default and one-letter titles.
On feat DEV 5bf8644 it fails 2/4: both 85% drops land before the tab.

Carved from #20549.

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

* CmuxNext: a tab dropped on a strip lands by the pointer (cx-6ggef)

An external tab drop resolved as if the pointer were the middle of a
phantom tab as wide as the strip's standard width, so the after slot
needed the pointer past the tab's right edge plus half a standard
width. resolvePhantom now hands resolveTab the pointer itself: the
slot edge nearest the pointer wins, where the ghost card sits. Group
drags keep their width-based placement.

Carved from #20549.

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

* test: pin the pointer-midpoint drop slot for a narrow tab (cx-6ggef)

resolveTab's doc and parameter still described the card's leading edge;
an external drop now passes the pointer. Say so, note the narrow-tab
hysteresis limit, and pin 15% and 85% drops on a 48 pt tab.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Already landed as #21626, the same change (cx-6ggef); every line of this diff is on feat-cmux-next. Closing.

This branch was previously deployed

1 inactive deployment
cmux-tui-merge-tree — 6d02cdb8 Deployed Oct 14, 2026 by teamleaderleo via publish the merge tree #629
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.

2 participants