Skip to content

ClosingPanes rules into cmux-layout-reducer (closing_panes) over the app FFI, step A: Rust port, C ABI, Swift split and parity corpus (no behavior change) - #35886

Merged
lawrencecchen merged 6 commits into
feat-cmux-nextfrom
swift-logic/closing-panes-reducer
Nov 4, 2026
Merged

lawrencecchen merged 6 commits into
feat-cmux-nextfrom
swift-logic/closing-panes-reducer

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Move 3 of the swift-logic plan (lead order): the springing-close rules of FRAME OWNER S3c/S3d (ClosingPanes: which panes border a closing pane and the gaps it keeps, where each side shows per frame, the camera carries, the panes nothing moves over, a taken-back close's velocities) move to cmux-layout-reducer (closing_panes), called through cmux-layout-reducer-ffi (cmux_closing_panes_*). This is step A of the repo's two-step app FFI move (as #35848): the Rust rules, the C ABI and the parity proof land now; the app keeps calling the Swift rules until the CCmuxAppFFI release that carries the new symbols is pinned. Step B (branch swift-logic/closing-panes-ffi-switch, head 189585c, ready) then switches Swift to the C calls and deletes the Swift rule copy.

Owner: PL-motion-core (layout motion, OWNERS.md; FRAME OWNER S3c/S3d by Lawrence Chen, #34170, #35140); you may take this over.

Red test first: I read ClosingPanes for a real defect (NaN or negative frames, a kept handle off the pixel grid, row-column scroll off-by-one, left/right rounding asymmetry) and found none that changes behavior: rects are clamped with max(0, ...), pixel placement is the caller's (pixelEdges), and the scroll travel restarts from where it is. So this move is behavior-neutral and proven by a corpus, not by a red test. One order dependence existed: the gaps' min()/max() were read in Dictionary order (seeded per process); they are now read in pane order (the neighbour lists were already sorted). Only the sign of a zero gap could differ, never its value.

What changes:

  • ClosingPanes+Rules.swift (ClosingPanesRules): the former ClosingPanes bodies with the geometry reads replaced by inputs, same order of operations; panes, scroll areas and row columns are numbers the wrapper hands out. ClosingPanes.swift keeps its API and every call site; it gathers the inputs and maps the answers.
  • ClosingPanesCorpusTests: 160 seeded scripts of closes (1,767 calls: 307 begin, 287 settle, 702 rects, 118 carry, 75 settle-carried, 125 row stops, 109 take-backs, 44 drops), recorded from the Swift rules on the fleet. Each line has the call's answer and an FNV-1a hash of the whole state after it (every side's neighbours, gap, place, travel start and end, carry, row travel, close order, awaiting set), so a difference anywhere in the state fails the line. Answers and state as IEEE-754 bit patterns.
  • cmux-layout-reducer/src/closing_panes.rs: the rules in Rust, one operation at a time (Swift's min/max and Sequence.min()/max()/min(by:) kept, so ties and NaN answer the same), with unit tests. rects (per display frame during a close) reuses the handle's buffers: no allocation in steady state.
  • cmux-layout-reducer-ffi/src/closing_panes.rs and the header: one handle per screen, typed calls, no JSON. From Keyboard resize rules in Rust: cmux-layout-reducer shown_resize over the app FFI, proven equal to Swift on a 3,203-case corpus (step A) #35848's review notes: outputs are written only on an answer; begin/remove return 1 for an answer and 0 for none, documented beside CMUX_LAYOUT_REDUCER_OK (also 0); the Safety text forbids an out pointer overlapping an input; struct sizes and field offsets are tested.

Numbers

swift-logic-lines.py (swift_logic_lines):

  • base feat e60197e: 415,963
  • this head (step A): 416,213 (+250: the split adds the gather code; the rules count until step B)
  • step B head 189585c (not in this PR): 415,646, -317 against the base. ClosingPanesRules becomes one cmux_closing_panes_* call per method (PLATFORM row, rule c); ClosingPanes is MIXED 1 (its live-frame "moving" test stays Swift).

Changelog

none

Observed / Not run

Observed (fleet):

  • Corpus recorded from the Swift rules: step cf0ccca08931af31dadbcbf3 (aws-m4pro-4) at a5b46f9 (branch fix/closing-panes-record-nomerge, this tree with record = true; never lands): recordCorpus passed; artifact sha256 1e485a58030f, the committed fixture. A first attempt (9332b0b03dcf) was cancelled by the worker (its same-tree cmux-tui build was terminated), not by a test.
  • Swift at 8cb2131 (the head differs only in closing_panes.rs): step f4501a147d3412e9a3455c03: ClosingPanesCorpusTests, PaneCloseKeptHandleTests, ClosedPaneContentStaysUntilLandingTests, PaneContentPolicyTests, PaneGroundCoversPaneTests: all 5 suites passed.
  • Rust at this head 97eff4c: cmux-tui-rust-check.sh test every_call_of_the_corpus step 77cce0044edb16fff9d4722e: closing_panes_corpus passed (answers and state, bit for bit); test closing_panes step c6c8c577faddb37a00444a42: 5 of 5 unit tests passed; clippy step f3525fd319e219ea600a0198: green.
  • C ABI at ad74896 (branch test/closing-panes-ffi-proof-never-merge: this head plus the FFI crate in the cmux-tui workspace, as Keyboard resize rules in Rust: cmux-layout-reducer shown_resize over the app FFI, proven equal to Swift on a 3,203-case corpus (step A) #35848's proof; never lands): corpus through the C calls (step 54801c299cc2d36ec92dca35), null and unknown-kind refusals (06cd9119083d47d4cafef4f8), sizes and field offsets (008991d207639346d93eca03): all passed; clippy step 0ded8428bd69816e8e654345: green. This PR's own CI runs the FFI crate's tests in its own workspace.
  • Earlier reds, fixed in 97eff4c: my unit test expected Swift's min(0, -0) to be -0 (Swift keeps its first argument; the rule was right, the expectation wrong); clippy asked for plain boolean forms and a loop for min(by:) (no behavior change; the corpus passed through both).
  • Bench (rects is perf-hot: 4ms motion-frame): PaneMotionBench (CMUX_PANE_BENCH=1) at the base and this head on one worker (aws-m4pro-1), steps 94e030691940801b9534b97d and d0f1866cb1acee3f3a5e6f03: 0 frames over 8.33 ms in both; 30 panes close p95 1.072 vs 1.024 ms, columnClose p95 0.908 vs 0.922 ms, max 3.5 vs 3.3 ms. An earlier unpinned run of this commit on another worker (fbc2ddf437a9, aws-m4pro-1 vs the base on aws-m4pro-3) went over 8.33 ms in column and columnClose; column does not run ClosingPanes, and the pinned rerun is clean, so it was worker load.
  • python3 scripts/cmux-next/prepush-gate.py: no new violations.
  • Expected red: the app FFI pin step of cmux-next checks (step A of the two-step path; cmux-app-ffi: generate cmux_motion_ffi.h with cbindgen, fail on drift #35307, feat(tab-drag): expose index and reveal rules over app FFI #34507, cmux-rd: one typed app line for cmux rd serve|connect, read back through a C line reader (ABI 5; Mac Screen step A) #35828 landed the same way). The tip shows it until the repin PR lands.

Not run:

  • pane-perf-terminal.py and anim-perf-terminal.py (the motion-frame bench's terminal and Chromium rows, light and stress): not run on this head. Correction (review 6643086790): this head does change the per-frame Swift path a little: rects now builds a [FrameFact] array (with a per-frame moving value it does not use) and a row set, and looks up each neighbour edge linearly where it used a Dictionary; PaneMotionBench on one worker (base 94e030691940801b9534b97d, head d0f1866cb1acee3f3a5e6f03) keeps that in noise: 0 frames over 8.33 ms, 30-pane close p95 1.024 -> 1.072 ms, columnClose p95 0.922 -> 0.908 ms; they are due at step B, which switches the per-frame call to the FFI.
  • The app compile of step B waits for the new CCmuxAppFFI pin (it imports the new symbols).

Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Ports the springing-close rules (ClosingPanes: which panes border a closing pane, the gaps it keeps, where each side shows per frame, the carries, take-backs) from Swift to cmux-layout-reducer (closing_panes), exposed through a C ABI (cmux_closing_panes_*) in cmux-layout-reducer-ffi. Step A of a two-step move: the Rust rules, C ABI and parity proof land now; the app keeps running the Swift rules until step B switches it to the C calls.

  • Splits the Swift rules into ClosingPanes+Rules.swift (the former bodies over plain numbers) and ClosingPanes.swift (a wrapper that gathers inputs and maps answers), with no change to the API or call sites.
  • Records a 1,767-call corpus of seeded closes from the Swift rules; the Swift, Rust and C ABI tests replay the same file, comparing each call's answer and a hash of the whole state bit for bit.
  • Ports the rules to Rust, keeping Swift's min/max tie and NaN behavior (a NaN row travel now means no travel, matching Swift); rects reuses the handle's buffers, so steady state allocates nothing.
  • The C ABI is typed with one handle per screen; begin and remove return 1 for an answer and 0 for none, and outputs are written only on an answer.
  • One behavior-neutral change: the gaps' min/max were read in Dictionary order (seeded per process) and are now read in pane order — only the sign of a zero gap can differ, never a value.
  • Reads the close order with get() instead of indexing to keep the panic ratchet (indexing_slicing 70 > 69).

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

View guided diff Turn on auto-fix

Review fixes (FIX FIRST at 97eff4c, comment 6643086790)

  • Red first: f5fcce2 adds closing_panes::tests::a_nan_row_travel_is_no_travel_as_in_swift. Fleet step 5301bde55a53ab7c139a6da5 (Linux, cmux-tui-rust-check.sh test a_nan_row_travel_is_no_travel): FAIL, assertion left == right failed at closing_panes.rs:779 (Rust took a NaN row travel; Swift's guard abs(...) >= 0.5 else { continue } skips it).
  • Green: be70ab9 skips a NaN travel as Swift does (let moves = abs >= 0.5; if !moves { continue }) and reads the close order with get() (the panic ratchet's new indexing_slicing hit at closing_panes.rs:551). Fleet step c52fcf7cae91ab1337219d0a (test closing_panes): 6 tests run, 6 passed. The PR's own clippy (changed cmux-tui packages) job, whose Rust panic ratchet step runs with --compare-base, passes at this head.
  • swift_logic_lines does not change (Rust only).

…, the geometry gathered in the wrapper; the rules ported to cmux-layout-reducer closing_panes with a C ABI (cmux_closing_panes_*); a seeded corpus test in both (no behavior change)

Move 3 of the swift-logic plan (lead order): the springing-close rules of
FRAME OWNER S3c/S3d leave Swift in two steps (the repo's app FFI path, as
#35848). This is step A's code; the corpus of answers is recorded from the
Swift rules on the fleet in the next commit.

- ClosingPanesRules (Swift): the former ClosingPanes bodies with the
  geometry reads replaced by inputs, in the same order of operations; panes,
  scroll areas and row columns are numbers the wrapper hands out. The
  wrapper keeps ClosingPanes' API and every call site.
- cmux-layout-reducer closing_panes: the same rules in Rust (Swift's min,
  max and Sequence.min/max kept, so ties and NaN answer the same), with unit
  tests. The per-frame rects reuses the handle's buffers (no allocation in
  steady state).
- cmux-layout-reducer-ffi closing_panes: one handle per screen, typed calls,
  no JSON; begin and remove return 1 for an answer and 0 for none (documented
  beside OK), outputs written only on an answer, the Safety text forbids an
  out pointer overlapping an input; struct sizes and field offsets tested.
- ClosingPanesCorpusTests (Swift), closing_panes_corpus (Rust) and the FFI
  test replay the same corpus: each call's answer and a hash of the whole
  state after it, bit for bit.

The neighbour lists were already sorted by pane id; the gaps' min and max
were read in Dictionary order (seeded per process) and are now read in pane
order: only the sign of a zero gap could differ, never a value.
…losing_panes_state_hash), so step B's Swift test still compares the whole state after each corpus call
…ose rules for 160 seeded scripts, 1,767 calls (recorded on the fleet: step cf0ccca08931af31dadbcbf3 at a5b46f9, artifact sha256 1e485a58030f)
… unit test's expectation); clippy: plain boolean forms and Sequence.min(by:) as a loop (no behavior change; the corpus passed through both)
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review be70ab9: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr35886 -f ref=swift-logic/closing-panes-reducer -f platform=macos -f backend_mode=local

red: cmux-next checks (god files, concurrency, crash safety, l10n); NEW: green on the base 7a89fde; do not merge until green; see lane-rules

Shipped in:

The feat-cmux-next push run https://github.com/manaflow-ai/cmux/actions/runs/41927927728 at b5e96a1 failed: cmux-next checks (god files, concurrency, crash safety, l10n).
Those jobs last passed at 3484771. This pull request is one of 5 merged since: #35886, #35875, #35873, #35889, #35898.
If the failure is in your change, fix forward on feat-cmux-next. Pull requests run only the tiers their paths reach (docs/ci/cmux-next-tiers.md); label a batch PR full-ci to run them all before merging.

@lawrencecchen cmux-next checks (god files, concurrency, crash safety, l10n) first failed at merged commit e241a67656894e048a0701ed173724a8f9e9c26d after green 34847716555ad6e148bc81cfcc595f144734e30d.
Bisected completed CI verdicts for every first-parent commit up to e241a67656894e048a0701ed173724a8f9e9c26d (https://github.com/manaflow-ai/cmux/actions/runs/41926109210); 0 midpoint lookups. The verified history is monotonic.
Skipped, missing, conflicting, infrastructure, or non-monotonic evidence stays unresolved.

@blacksmith-sh

blacksmith-sh Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Found 6 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmpo4tzkdo7/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmpu1wwll6r/summary-missing.json (live):/summary-missing.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs

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

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent): FIX FIRST at 97eff4c

One red of this PR's own blocks it. The port is otherwise correct and the evidence holds. Push the one-line fix (and the NaN guard below in the same push), then this is SHIP on that head if the ratchet goes green and nothing else changes.

Blocking

  1. Panic ratchet red caused by this PR (clippy (changed cmux-tui packages), step "Rust panic ratchet on changed cmux-tui packages", job 126885447766, run with --compare-base, so only hits HEAD adds count): cmux-layout-reducer: clippy::indexing_slicing 70 > baseline 69, new hit at cmux-tui/crates/cmux-layout-reducer/src/closing_panes.rs:551:24 (let pane = self.order[index];). The fleet clippy step was green because plain clippy does not turn on the ratchet's lints. Fix: for &pane in self.order.iter().rev() { (the loop body only borrows the other fields: entries, shown, rests, values, placed), or let Some(&pane) = self.order.get(index) else { continue };. Do not raise the baseline and do not add an allow. The result does not change, so the corpus stays valid. (arithmetic_side_effects 61 > 58 on the same crate is a warning tier, not red.)

Should fix in the same push (not blocking alone)

  1. NaN goes a different way in settle, closing_panes.rs:536: if (travel.target - travel.now).abs() < 0.5 { continue; }. Swift has guard ..., abs(travel.target - travel.now) >= 0.5 else { continue } (ClosingPanes+Rules.swift:231). With a NaN travel, Swift skips and Rust sets row_travel to NaN. After that, still reports the pane as covered in Rust and as still in Swift. The module doc says every comparison has the Swift form "so NaN and ties give the same answer", and the corpus has no NaN input, so it cannot catch this. Write it as if !((travel.target - travel.now).abs() >= 0.5) { continue; }. Every other comparison I checked matches the Swift form, NaN included.

Checks

  1. Swift split behavior-neutral: yes. I compared base ClosingPanes.swift with head ClosingPanes.swift + ClosingPanes+Rules.swift line by line: begin (neighbour scan, the 0.5/1 pt tolerances, Swift.max(0, gap)), handleStays (eligibility per handle kind maps exactly; handle order does not matter, the result is an OR), side gap/widest, settle, rects (place/collapse/resting, same order of operations), carry, settleCarried, shiftY, remove (min(by:) tie keeps the first), still, drop. Neighbour order: base sorted by rawValue. Head lists before.panes.sorted { $0.key < $1.key }, and LayoutIdentifier.< is rawValue <, so the order is the same (this matters for the remove velocity tie-break). The nil→0 numbering is injective for each kind, so the Optional == area and row-column tests map exactly. A row edge's column is never 0, so it never matches a nil row column (as before). row(column) is now called once for each distinct column per frame, not once for each entry. rowScrollTravel is pure, so this is the same. The wrapper's number maps reset with releaseClosing (closingPanes = ClosingPanes()), so they are bounded.
    Order fix: invisible. A -0 gap needs rest.minX == -0.0 and was.maxX == +0.0 (Swift.max(0, -0.0) returns -0.0), and the order only matters when one side has both a +0 and a -0 gap. Where the sign goes: fixed = value + sign*gap → the rect origin/size as ±0 → offsetBy/pixelEdges → host.frame (!= and drawing treat ±0 as equal). In remove it goes into abs(edge - value) (same magnitude). The divisions in collapse have abs(denominator) >= 0.5 guards, so a signed zero never reaches a division. Only the state hash bits can change, and the corpus was recorded after the fix. No red test is needed.
  2. Rust = Swift rules: yes on every reachable branch, apart from the NaN guard above. swift_min/swift_max/least/most/min(by:) match the stdlib forms (y < x ? y : x, y >= x ? y : x, first-wins on ties). The handle eligibility forms match (handle.docked != scrolls ≡ !docked == scrolls). Rust has no FP contraction, and the corpus is bit-exact on Linux against Swift recorded on arm64. Corpus: 160 scripts, 1,767 calls, every op kind with minimum counts and minimum answered counts in both languages, and assert_eq! on every line, so a mismatch fails. Hash: sound. It covers all rule state a later call reads (every side's neighbours, gap, fixed, start, end, scrolls, row column, carry, row travel, order, awaiting), not the scratch buffers. Swift sorts the keys as Rust's BTreeMap does. Coverage gaps (fine for step A): no NaN/inf and no ±0 gap inputs, and no re-begin of a pane that is already closing.
  3. C ABI: OK. The header matches the Rust #[repr(C)] structs and the signatures. Sizes and offsets are tested. Every entry point has catch_unwind. Null pointers give ERR_NULL and an unknown handle kind gives ERR_INVALID, with nothing written (tested). out is written only on an answer. 1/0 vs OK is documented in the module and the header. The Safety text forbids out overlapping an input or the handle. rects reuses frames/rows/placed and the rules' buffers: no allocation in steady state. Note: settle allocates a BTreeSet and a Vec per call (once per change, not per frame, so fine).
  4. Per-frame cost: the step-A evidence is enough, but one statement in the body is not accurate. "This head does not change what the app runs per frame" is wrong: the Swift rects path now builds a [FrameFact] (with a 4-element moving array per live frame, ClosingPanes.swift:177, unused by rects), a Set plus [RowTravel], and does a linear frames.first { $0.id == id } per neighbour edge where the old code did a Dictionary lookup (O(C·N·F) per frame). The same-worker PaneMotionBench (94e030691940 vs d0f1866cb1ac: 0 frames > 8.33 ms, 30-pane close p95 1.024 → 1.072 ms, columnClose 0.922 → 0.908 ms) shows this stays within noise and far under the 4 ms mark. Please correct the sentence. Step B must show: pane-perf-terminal.py and anim-perf-terminal.py terminal and Chromium motion-frame rows, light and STRESS, base vs the step-B head on one worker, p50/p95/p99/max and 0 drops. It must also show the Swift-side marshalling cost: step B's rects allocates frameFacts, c(frames), c(rows), the result array and the output dictionary each frame. Write C structs into a reused buffer if the rows move. Also for step B: ClosingPanesRules becomes a struct that holds a class handle, so copies of ClosingPanes share Rust state. Confirm that no copy is mutated on its own.
  5. Wiring: OK. At this head, cmux-tui/crates/cmux-layout-reducer-ffi (own workspace, job 126885469379) ran closing_panes.rs: the corpus through C, sizes/offsets, and null/kind refusals, all passed. cmux-next fast tier (cargo) (entry "sidebar layout reducer", -p cmux-layout-reducer) passed. Fleet steps 77cce0044edb (Rust corpus at 97eff4c) and f4501a147d34 (the 5 Swift suites, corpusMatchesTheFixture passed, recordCorpus skipped) agree. 8cb21315..97eff4cf touches only closing_panes.rs. The never-merge proof ad74896a0198 = this head + cmux-tui/Cargo.toml member, Cargo.lock entry, and the [workspace] line removed. Nothing else.
  6. Size: OK. The +250 is the fact structs plus the gather. Step B (189585c) keeps both and replaces the rule bodies with C calls, net -317. The split is not larger than step B needs. Step B could still remove the Swift fact structs by building the C structs directly.
  7. Reds this PR causes: (a) the panic ratchet above (not expected, blocking). (b) The expected app FFI pin step in cmux-next checks, still running when I looked. cmux-next swift test, tooling-tests and cmux-next CI guard group were still pending. The cancelled risk evidence/merge freeze/pr-*-check runs are superseded duplicates, and each has a green rerun at this SHA.
  8. Risk/owners: risk:normal is correct (no auth, socket, migration, release or signing; the C ABI is not linked by the app until step B). Owner: layout motion is PL-motion-core. ClosingPanes*.swift is not a listed path, and the body names the owner and the S3c/S3d author.

Not run by me: no build or test. I read the diff, the base, step B and the proof branch, and read the CI and fleet logs named above.

…in Swift (red: Rust skips only abs < 0.5, so NaN travels)
…s; read the close order with get() (panic ratchet: indexing_slicing 70 > 69)
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent): SHIP at be70ab9

I checked the delta 97eff4cf43db..be70ab937504. It changes only cmux-tui/crates/cmux-layout-reducer/src/closing_panes.rs, in three places. Both findings from my earlier review are fixed.

The delta

  • Ratchet hit (:551): the loop now reads let Some(&pane) = self.order.get(index) else { continue };. index always comes from 0..self.order.len(), so get always returns a value and the result does not change. This head's clippy (changed cmux-tui packages) passes, with the panic ratchet in --compare-base mode.
  • NaN row travel (settle, :536): the check is now let moves = (target - now).abs() >= 0.5; if !moves { continue }. This is the same as Swift's guard abs(travel.target - travel.now) >= 0.5 else { continue } (ClosingPanes+Rules.swift:231). A NaN distance now skips the travel in both languages.
  • Red test a_nan_row_travel_is_no_travel_as_in_swift: it is red for the right reason.
    • It builds a closing pane in row column 7 that has a neighbour on its min-y side only. This passes the isEmpty != isEmpty guard. It then calls settle with now: NaN.
    • At f5fcce2 (fleet step 5301bde55a53, the cmuxterm-hq run log), it fails at closing_panes.rs:779:13 with left: Some(Travel { start: NaN, target: 50.0 }) right: None. That is the bug exactly.
    • At this head, step c52fcf7cae91 runs all 6 closing_panes unit tests, and all 6 pass. The test also covers the Swift guard: it reaches the >= 0.5 comparison and no earlier continue.
    • Nit, not blocking: if let Some(entry) = closing.entries().get(&2) cannot be None there, because settle never removes an entry. A direct closing.entries()[&2] would make that clear.

Other comparisons checked for NaN

Every other comparison has the same form in Rust and Swift:

  • collapse:
    • rescroll: abs(scroll.target - travel.target) >= 0.5
    • travel end moved: free.end ... abs(old - end) >= 0.5 == true
    • rider share: abs(rest - riderStart) >= 0.5 (a match guard in Rust)
    • scroll share: abs(length) < 0.5 ? 1 : ... (:267)
  • begin:
    • the gap window: >= -0.5 && <= maxGap
    • the overlaps: > 1
    • the handle test: along >= lo - 0.5 && along <= hi + 0.5 && overlap > 1. Rust writes if !(a && b && c && d) { continue }, which is the same as the guard.
  • carry: drift != 0
  • remove: the strict < in min(by:)
  • swift_min, swift_max, least, most

None differs on NaN.

CI at this head

  • Red: only cmux-next checks. Its only red check is "Check the app FFI pin", which is the expected step-A red. The FAIL [tabs] / FAIL [new_workspaces] lines in that log come from a script self-test that reads made-up summary files (/tmp/tmp*/summary-*.json). They are not red checks.
  • Green: clippy (changed cmux-tui packages), cmux-tui/crates/cmux-layout-reducer-ffi, rust workspaces, cmux-next fast tier (cargo), risk evidence and merge freeze.
  • Cancelled: PR size, UI review advisory, classic-api-guard and the pr-*-check / risk evidence duplicates were cancelled. They are superseded runs.
  • Still running when I checked: cmux-next swift test and cmux-next fast tier (Swift) were queued.

Carried over from my earlier review (not blocking step A)

  • Correct the PR body's claim that "this head does not change what the app runs per frame".
  • Step B must show the terminal and Chromium motion-frame rows, light and STRESS, on one worker, with the Swift marshalling cost per frame.
  • Step B must confirm that no copy of ClosingPanes is changed on its own once it holds a class handle.

Not run by me: no build or test. I read the delta, the red and green fleet logs (cmuxterm-hq runs 41920927806 and 41920927525) and this head's CI logs.

This branch was previously deployed

3 inactive deployments
artifacts — be70ab93 Deployed Nov 4, 2026 by lawrencecchen via publish tree-addressed cmux-tui #113222
sentry-dsyms — be70ab93 Deployed Nov 4, 2026 by lawrencecchen via upload cmux-tui dSYMs to Sentry #113222
ci-routing-agent — be70ab93 Deployed Nov 4, 2026 by lawrencecchen via namespace autofix #14628
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant