Skip to content

feat(cmux-tui): set-layout-sizes commits a divider batch as one change or none (layout-sizes-batch-v1; daemon only, no app caller yet) - #34928

Merged
lawrencecchen merged 5 commits into
feat-cmux-nextfrom
feat/layout-sizes-batch
Nov 4, 2026
Merged

lawrencecchen merged 5 commits into
feat-cmux-nextfrom
feat/layout-sizes-batch

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

No change a person can see; the P0 resize rule does not apply. This PR adds a daemon command that no app path sends yet:

  • git grep -i -E 'set-layout-sizes|set_layout_sizes|layout-sizes-batch|setLayoutSizes' on feat-cmux-next d422b94 finds 0 hits in Packages, webviews, ios and web.
  • The existing commands that a divider drag or a key resize sends today (set-split-ratio, set-viewport-pane-width) are unchanged: the PR removes 0 lines in the daemon source files (mux.rs, mux/resource_topology.rs, mux/resource_topology/layout_ops.rs, server.rs, server/capabilities.rs, server/cmd_panes.rs, server/command_dispatch.rs, server/responses.rs). Its other files are new files, the spec, and generated bindings.
  • The identify capability list gains layout-sizes-batch-v1. The app reads that list only through capabilities.contains(<name>) checks, and no app code names this capability.
    The app change that sends set-layout-sizes (KEYS owner) changes what a person sees. That PR carries the display evidence of the P0 rule (the resize-keyboard journey or a divider-drag display run on a gui-session-v1 worker, at its head).

A key resize or a drag release that ends with 2 or more divider values (a keyboard column trade's two widths, an Option or Command drag's widths and ratios: EndedValues, #34123) went to the daemon as one set-split-ratio or set-viewport-pane-width per value under one transaction. Each was its own commit, so a value the daemon refused after it applied another left that one applied; the app kept all-or-nothing only on its own side (EndedValues.swift: "Two commands are still two daemon commits"). CLAUDE.md: batch requests apply as one transaction. (R-DAEMON-STATE DESIGN 2.10.1, lead item 2026-11-04.)

New command set-layout-sizes {values: [{split, ratio} | {pane, width}, ...], transaction}, capability layout-sizes-batch-v1:

  • One commit. One resource plan (screen.layout_sizes.set) applies every value, in order, to one layout snapshot of the screen, with the checks of its single command (unknown split, row split, the ratio's implied column width computed from the batch's own earlier widths, width range, pane not resizable), then commits once: one durable screen, one layout revision, one undo entry keyed by the resize transaction (the values a drag sent while it moved, under the same transaction, stay in the same undo step), one layout-changed event with the transaction echo.
  • Or nothing. A refused value refuses the plan before anything is staged: the reply carries the single command's error_code, and its message names the value's index (value 1: ...). Values on two screens, no value or more than 64 values are refused too. A batch that changes nothing commits nothing.
  • Spec (commands.md), SDK schema, inventory and api-compat surface updated; bindings regenerated (bindings/codegen/generate.py --write).

The app side is next (KEYS owner, frame-owner-keys): EndedValues.send sends one set-layout-sizes when the daemon advertises layout-sizes-batch-v1, else the per-value commands as today.

Testing

Red first, focused command ./scripts/verify-cmux-tui-hosted.sh --filter layout_sizes (Linux test (linux)):

Compile: fleet compile check job 4b6311261f683aade3e7f6db at ca012e6: CmuxNext (386 s), app (102 s) and cmux-tui (75 s) passed. lint (linux) at the head: no clippy error; the panic ratchet rows are base rows (cmux-tui-core arithmetic 1007 > 875, as on feat).

Observed / Not run

  • Observed: the hosted runs above, the fleet compile, the schema check and the binding check.
  • Not run: the app side (not in this PR); macOS execution of the focused tests (Linux only).

Changelog

none

Proof

No capture: daemon command only; the app does not send it yet.


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


Summary by cubic

Introduces set-layout-sizes (layout-sizes-batch-v1), a daemon command that commits a resize gesture's final divider values as one change or not at all.

  • Currently a key column trade or drag release ending with two or more divider values sends one set-split-ratio or set-viewport-pane-width per value under one transaction, each its own commit, so a value the daemon refuses after applying another still leaves that one applied.
  • The new command stages all values in order on one layout snapshot, running each single command's checks, then commits once: one layout revision, one undo entry keyed by the resize transaction, and one layout-changed event. A refused value refuses the whole batch before anything is staged, returning the single command's error code and the failing value's index; values across two screens, empty batches, batches over 64 values, and no-op batches are refused or commit nothing.
  • Spec, SDK schema, api-compat surface, and inventory updated, with all language bindings regenerated. The app-side caller that sends the new command is a follow-up.

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

View guided diff Turn on auto-fix

…e or not at all (R-DAEMON-STATE 2.10.1 red)

Mux::set_layout_sizes_in_transaction_checked_as takes a batch of divider values (split ratios, column widths)
under one resize transaction. This red commit applies them as today's app does, one commit per value, so a
refused second value leaves the first applied and a batch is two layout changes: expected red in
a_refused_second_value_leaves_the_first_unapplied and a_batch_is_one_layout_change_and_one_undo_step. The drag
plus release undo step and the count refusal pass on both (guards).
…ut revision, one undo step, or nothing (R-DAEMON-STATE 2.10.1)

set_layout_sizes_in_transaction_checked_as builds one resource plan (screen.layout_sizes.set): every value is
applied, in order, to one layout snapshot of the screen with the checks of its single command (unknown split, row
split, the ratio's implied column width from the batch's own earlier widths, width range, pane not resizable, one
screen), then one commit: one durable screen, one layout revision, one undo entry keyed by the resize transaction
(a drag's earlier values under it stay in the same step), one layout-changed event with the transaction echo. A
refused value refuses the plan before anything is staged, with the single command's typed error and its index; a
batch that changes nothing commits nothing.
…ues as one change (layout-sizes-batch-v1)

The wire command for the batch (R-DAEMON-STATE DESIGN 2.10.1): values are {split, ratio} or {pane, width}, 1 to
64 on one screen, with the gesture's resize transaction. One commit and one undo step, or nothing: a refused value
answers with its single command's error_code and a message naming its index. Spec (commands.md), SDK schema and
inventory updated; bindings regenerated (bindings/codegen/generate.py --write).

Test: server::tests::layout_receipts_shutdown::set_layout_sizes_applies_a_trade_whole_or_refuses_it_whole.
@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 ca012e6: 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=pr34928 -f ref=feat/layout-sizes-batch -f platform=macos -f backend_mode=local

red: cmux-next checks (god files, concurrency, crash safety, l10n); NEW: red check: Base-red classifier (per test) (+1 more) is not red on the base 4a1d91d; do not merge until green; see lane-rules

Shipped in:

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (claude-reviewer-rds-batch): SHIP at ca012e6
reviewer-session: 42b6dc5f-f0d4-4966-9cb7-ed3fcdc751c2
author-session: ae25c6d154f94c01a
R9: pass | Layout row ("Layout: workspaces, screens, columns, panes, tabs, groups, pins, names, colors"; owner Rust, single writer the workspace store reducer / cmux-tui-core::state). set-layout-sizes writes column widths and split ratios only through one daemon resource plan (screen.layout_sizes.set in commit_resource_mutation_plan: one UpsertScreen, one journal record), under the same registry and state locks as set-split-ratio and set-viewport-pane-width. Clients send only the intent. The row keeps one writer.
perf claim: none

No defect must be fixed before merge. I read the full non-generated diff against the singles (set_split_ratio_inner, set_viewport_pane_width_inner, resource_set_split_ratio, resource_set_viewport_width) and the plan runner (commit_resource_mutation_plan, stage_checked).

Verified

  1. All or nothing. Every refusal returns from the prepare closure (layout_ops.rs:316) before stage_checked. The prepare only reads state. The typed error goes through anyhow::Error::new with no context (defer_session_paths does not wrap it), so downcast::<LayoutSizesError>() gets it back. LayoutSizesUnchanged returns Ok with no commit, no layout revision, no undo entry and no event. A store failure leaves state unchanged: apply never ran, or commit_resource_mutation_plan restores the staged clone. The batch then answers NotPersisted and emits Status, as the singles do.
  2. Value checks match the singles. The batch checks each value as its single command does: the ratio clamp, split_screens screen and owner, row-split refusal, the column split's implied width read from layout.layout_columns (so earlier batch widths count), the root ratio, the width range and the pane's column. sync_layout_column_projection runs once and only when a width changed, as in resource_set_viewport_width. Column ratios keep set_layout_split_ratio's sync_layout_column_widths, as the single does. I found no check that only one side makes, except B below. One small difference: the batch's apply also writes creation_order_auto_layout and the width single does not. On a consistent column screen this has no effect, and the batch's behavior is the more complete one.
  3. Undo. The batch uses the same key, Resize{ControlClient(client), transaction}, and the same layout_snapshot_for_coalescing_change + record_prepared_layout_change pair as the singles. So a drag's singles and the release batch are one undo step (a_release_batch_joins_its_drag_in_one_undo_step proves this). The batch emits one emit_layout_changed with the decimal echo.
  4. Concurrency. The batch does its with_state lookups and ordinary_pane_selectors before the plan. The screen lookup for the event runs after the plan. The prepare and apply closures use only the state and registry they get as arguments. The lock order is registry, then state, as in every plan. There is no new order and no re-entry.
  5. Wire and lanes. Remote clients cannot run the new command. remote_relay::intercept denies it (_ => Err(denied())), and the remote frame gate, DaemonLanePolicy.allowedCommands and cmux-mobile-host ALLOWED_COMMANDS are all allowlists. The incognito_remote request walk goes into values[*].split and values[*].pane. A value with neither pair fails to decode (the wire test shows this). For a value with both pairs, see A.
  6. Schema. I ran check-sdk-schema.py (ok, 290 commands), generate.py --check (ok, all languages), check-spec-inventory.py (ok) and scripts/cmux-next/gen-phone-lane-fields.py --check (ok). The inventory control list, commands.md, sdk-schema (min_items 1, max_items 64) and the api-compat surface agree with each other. The api-compat and inventory jobs pass.
  7. CI.
    • The red run 41778535469 failed exactly the two batch tests: ([0.4, 0.5], 2, 2) vs ([1.0, 0.5], 1, 1), and revision 3 vs 2.
    • The green run 41778538160 passed 4 of 4.
    • At the head, test (linux) passed 5 of 5, in MODE: focused, TEST_FILTER: layout_sizes.
    • At the head, lint (linux) passed fmt and clippy -D warnings. Its red ratchet rows are the same at base b4570f1 (run 41776877735, lint job 126403785453): acpmux, cmux-automations and cmux-tui indexing_slicing, and cmux-tui-core arithmetic 1007 > 875 (warn). These are base findings.
    • cmux-next checks is red only on scrollbars, a gallery fixture host name and the base-red classifier (ci-guards.yml). This PR touches none of those files, so these are base findings.
    • At 801ba62, the macOS job failed on unused_qualifications and an unused import. abb1b32 fixed both. The head macOS job is still queued.

Non-blocking findings

  • A. server.rs:2408-2413: a value with both pairs is half applied. A value such as {"split":4,"ratio":0.5,"pane":2,"width":0.4} decodes as SplitRatio. The daemon then drops the width and answers ok:true. The schema says additional_properties: false, and the command promises every value or none. Fix: make the two variants newtypes over two structs with #[serde(deny_unknown_fields)], and add this case to the wire test. The generated bindings make this shape unlikely, but a hand-written client can send it.
  • B. layout_ops.rs:439 and 481-488: the batch does not refuse a non-finite ratio. The single refuses it (layout_ops.rs:100 ensure!(ratio.is_finite() ...)). JSON cannot carry NaN, so the wire cannot reach this. An in-process caller of the public Mux::set_layout_sizes_in_transaction_checked_as with a NaN ratio commits a NaN split ratio. One is_finite check fixes it. A cosmetic edge in the same area: at layout_sizes.rs:119-123, when value 0 is a ratio and the screen's active pane has no public id, the answer is viewport-column-not-found for a pane the caller never sent. The single answers layout-ratio-target-missing here. This cannot happen on a consistent screen.
  • C. Test gaps. No test sends a SplitRatio value. So the ratio arm, the active-pane selector path and the "implied width from the batch's own earlier widths" claim have no test. These also have no test: the other-screen refusal, the no-op batch, the one layout-changed event with its echo, and the advertised capability. Suggested tests:
    • Three columns, [width c0 = 0.4, ratio s2 = 0.5]: expect c2 = c0 + c1 as the batch set them, revision +1 and undo +1.
    • Two screens: expect layout-sizes-other-screen at index 1, and nothing changed.
    • Current widths sent again: expect Ok, the same revision and undo depth, and no LayoutChanged.
    • advertised_capabilities(false).contains(&LAYOUT_SIZES_BATCH_CAPABILITY), as the sibling capability tests do.
  • D. Docs.
    • cmux-tui/spec/events.md:1013 lists the commands whose transaction the layout-changed event echoes. Add set-layout-sizes to that list.
    • DESIGN 2.10.1 says that values on two screens answer invalid-argument. The code and commands.md use layout-sizes-other-screen. Make the design text match.
  • E. server.rs is over its god-file hard ceiling (warn-only). It has 3022 lines, against a ceiling of 2978 (it had 2996 at base). LayoutSizeWire and its From impl (17 lines) could move next to cmd_panes::set_layout_sizes.

Observed / Not run

  • Observed: the diff and the code paths above; the four python checks; the job logs named above.
  • Not run: no cargo, rustc or swift build on this machine. The full cmux-tui-core suite did not run on this PR, because the hosted test was focused. I read the suites that list every command (remote_relay/tests.rs only_the_section_4_commands_pass_the_gate, remote_entry_tests.rs). They should still pass, because the new command is denied for remote clients. The app side is not in this PR.

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Nov 4, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Base finding, not this PR's own red: cmux-next checks fails on two base checks at this head (run 41784210160, job 126449561018).

@lawrencecchen lawrencecchen changed the title feat(cmux-tui): set-layout-sizes commits a resize gesture's divider values as one change or none (layout-sizes-batch-v1) feat(cmux-tui): set-layout-sizes commits a divider batch as one change or none (layout-sizes-batch-v1; daemon only, no app caller yet) Nov 4, 2026
@lawrencecchen
lawrencecchen merged commit d9507dc into feat-cmux-next Nov 4, 2026
366 of 396 checks passed
@lawrencecchen
lawrencecchen deleted the feat/layout-sizes-batch branch November 4, 2026 08:39
@cmux-bot cmux-bot Bot removed the stack-blocker label Nov 4, 2026
lawrencecchen added a commit that referenced this pull request Nov 4, 2026
…34928 review B, on the path it named)

a_non_finite_ratio_is_refused_and_changes_nothing used a column split, where the implied width already refuses a
NaN, so it passed before the fix. A tree split takes the ratio as it is: this is the path the review named.
lawrencecchen added a commit that referenced this pull request Nov 4, 2026
…e pair and refuses a non-finite ratio; tests and docs (#34928 review follow-ups) (#35261)

* test(cmux-tui-core): set-layout-sizes refuses a value with both pairs and a non-finite ratio; covers the ratio arm, two screens, a no-op batch, one layout event and the capability (#34928 review follow-ups, red)

Red: a value with both a ratio and a width decodes as a ratio and drops the width (review A); a NaN or infinite
ratio is committed in-process (review B). The rest pins behavior that is right already (review C):
a_ratio_value_uses_the_widths_the_batch_set_before_it, values_on_two_screens_are_refused_whole,
a_batch_of_the_current_sizes_commits_nothing, a_batch_sends_one_layout_changed_event,
identify_advertises_the_layout_sizes_batch.

* fix(cmux-tui-core): set-layout-sizes refuses a value that is not exactly one pair and a non-finite ratio; its wire type moves to cmd_panes (#34928 review follow-ups)

- Review A: LayoutSizeWire's variants are newtypes over two structs with deny_unknown_fields, so a value with both
  {split, ratio} and {pane, width} (or any other field) is refused, not decoded as a ratio with the width dropped.
- Review B: a NaN or infinite ratio is refused in-process (LayoutSizesError::NonFiniteRatio, invalid-argument),
  as the single command refuses it; the clamp keeps a NaN.
- Review D: events.md lists set-layout-sizes among the commands whose transaction layout-changed echoes; commands.md
  states the strict value shape. (DESIGN 2.10.1 already names layout-sizes-other-screen.)
- Review E: LayoutSizeWire and its From impl move from server.rs to server/cmd_panes.rs (server.rs 3022 -> 3006
  lines; still over its warn-only ceiling of 2978). The Command field keeps the name, so check-sdk-schema.py maps
  it as before; generate.py --check and check-sdk-schema.py pass.

Tests (red at efe6dbf): set_layout_sizes_applies_a_trade_whole_or_refuses_it_whole (the both-pairs case),
a_non_finite_ratio_is_refused_and_changes_nothing.

* test(cmux-tui-core): a non-finite ratio on a tree split is refused (#34928 review B, on the path it named)

a_non_finite_ratio_is_refused_and_changes_nothing used a column split, where the implied width already refuses a
NaN, so it passed before the fix. A tree split takes the ratio as it is: this is the path the review named.

* test(cmux-tui-core): set-layout-sizes refuses an array value and clamps an infinite ratio as set-split-ratio does; the event echoes its transaction (#35261 review, red)

Red at c7b0434: the newtype-over-struct LayoutSizeWire accepts [split, ratio] (serde gives a plain struct a
sequence form; untagged struct variants have none), and NonFiniteRatio refuses an infinite ratio that
set-split-ratio clamps to 0.95 (JSON 1e39 reads as f32 infinity). Also: a ratio value on the wire, the bad-request
shape of a refused value, the layout-changed event's transaction echo.

* fix(cmux-tui-core): set-layout-sizes values are untagged struct variants with deny_unknown_fields; only a NaN ratio is refused (#35261 review)

- Review 1: back to struct variants, with #[serde(untagged, deny_unknown_fields)] on the enum: a value with both
  pairs or another field is still refused, and an array value is refused again (serde gives untagged struct
  variants no sequence form; the newtype structs had one).
- Review 2: an infinite ratio clamps to 0.95 as set-split-ratio clamps it ('same clamping and ranges' in
  commands.md). Only a NaN is refused (LayoutSizesError::NanRatio, invalid-argument): in-process callers only, the
  clamp keeps a NaN and the store cannot persist one (before, the batch answered layout-sizes-not-persisted).
- The tests match the variant (NanRatio) where they only matched an index.

Tests (red at 20f0fa4): set_layout_sizes_applies_a_trade_whole_or_refuses_it_whole (the array case),
a_nan_ratio_on_a_tree_split_is_refused_and_an_infinite_one_clamps.

This branch was previously deployed

3 inactive deployments
artifacts — ca012e6b Deployed Nov 4, 2026 by lawrencecchen via publish tree-addressed cmux-tui #109950
sentry-dsyms — ca012e6b Deployed Nov 4, 2026 by lawrencecchen via upload cmux-tui dSYMs to Sentry #109950
ci-routing-agent — ca012e6b Deployed Nov 4, 2026 by lawrencecchen via cmux-tui tree preflight #109950
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