Skip to content

Right column: measure panes by their final layout width, so an open right after a split is a tab, not a split below - #30010

Merged
lawrencecchen merged 4 commits into
feat-cmux-nextfrom
fix/pane-width-final-layout
Nov 1, 2026
Merged

lawrencecchen merged 4 commits into
feat-cmux-nextfrom
fix/pane-width-final-layout

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Right after a split, a link or an agent open beside a pane could land as a new split below the pane on the right. It should be a tab in that pane. This happened in windows narrower than about 960 pt, and only while the split's spring ran, so scripts and agents that split and then open at once hit it most.

Cause: LinkOpener.width(of:) read the pane view's bounds. While the split's spring runs, the source still shows its width from before the split, because its content is held at that size until the spring lands. The new pane starts as a zero-width sliver, which reads as nil, and nil sets no width limit. So RightColumnPlacement.decide saw a wide source and an unmeasured right pane, and picked splitBelow. With both panes measured at their final widths, it picks tab. Found by the independent review of #29787.

Fix: width(of:) now reads LayoutRootView.restingContentWidth(of:). This is the pane's frame in the geometry its screen's springs head for (the baseGeometry the right column is already read from), less the pane padding. The width now comes from one place, so both entry points get the fix:

  • links: LinkOpener.target(from:), which covers openWeb, openFile, openFolder and chat-dock tools through ChatColumnPlacement.openTool
  • agent opens: LinkOpener.rightColumn(beside:), which covers AgentTabStore.rightColumn (AgentSidePanes, AgentSubagentPanes) and AgentBesidePlacement

Action budget: the new read is a dictionary lookup into geometry that is computed when the layout syncs. It adds no layout pass and no wait to a handler. A pane that no screen view lays out (a pane with no workspace, as in SideOpenStripTests) still reads nil, as before.

Owners: LinkOpener and RightColumnPlacement have no OWNERS row, so they go to the Performance Lead. The placement rule is Leo's (#28989, #29684) and is unchanged. LayoutRootView.swift is in the "Layout motion" row (PL-motion-core); the only change there is one new read-only accessor.

Testing

cmux-ci run --class light --display --script scripts/ci/package-test-lane.sh --arg=suite --arg=Packages/macOS/CmuxNext --arg=RightColumnAfterSplitTests --env CMUX_TEST_REQUIRE_GUI=1

New test RightColumnAfterSplitTests.anOpenRightAfterASplitReadsThePanesFinalWidths. It uses a same-tree daemon and a 940 pt window. The test drives the layout clock itself (driver.isManual), so the spring stays at its first frame on every host. It splits right, then at once:

  • checks the source's measured width
  • checks the agent path's decision through services.agentTabs.rightColumn
  • opens a link

It expects a tab in the terminal-only right pane and no new split.

  • Red at ade9546b00de (test only, merged with feat-cmux-next), job 2f3bc17c13cb08a78ffb1630 on cmuxs-Mac-mini-4. Three failures:

    • the source read 720 pt (its width before the split)
    • the agent column was Column(sourceWidth: 720, widths: [:]), decided as splitBelow
    • the link target was splitBelow
  • Green at 3af8e0a05961 (head), job a6ce3d5aa1dfab7b603114df on cmux13s-Mac-mini. These suites passed: RightColumnAfterSplitTests, RightColumnPlacementTests, LinkOpenPlacementTests, RightColumnTurnTests, SideOpenStripTests, RightColumnScreenTests, RightColumnEntryPointTests.

  • Display proof at the head 3af8e0a05961: job 0b3b961d1c808367eb9bf7f5 (gui-session-v1, cmux8s-Mac-mini), state done. All 7 suites passed: RightColumnAfterSplitTests, RightColumnPlacementTests, LinkOpenPlacementTests, RightColumnTurnTests, SideOpenStripTests, RightColumnScreenTests, RightColumnEntryPointTests.

Observed / Not run

  • Observed in the same green job: ClickedLinkFocusTests passed 8 of 10 tests. It now passes aLinksTabInAnotherPane... and aChatChipClickedAgain... on cmux13, the host where base red: ClickedLinkFocusTests' pane on the right holds a browser, so a link lands there on any host #29787 saw them fail. At the base they still fail (job 47791f5f3045e15de53ef5b2 at ade9546b, cmux-austin-mini-0). So this race was a cause of those failures.
  • Base findings, not caused by this PR: these tests fail the same way at the base ade9546b (job 47791f5f3045) and at the head:
  • Not run: the full CmuxNextAppTests target, and a live tagged build.
  • Not run: a hidden or parked workspace. It now reads the geometry from when it last laid out, not nil. I read this from the code and did not test it.

Changelog

  • Fixed: A link or agent panel opened right after a split now opens as a tab in the pane on the right, not as a new split below it, in narrow windows.

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


Summary by cubic

Fixes placement of links and agent panes opened right after a split: instead of landing in a new split below the pane on the right, they now land as a tab in that pane, in narrow windows.

LinkOpener.width(of:) read a pane's view bounds, which are stale while a split's spring runs: the source still shows its width from before the split, and the new pane starts as a zero-width sliver (nil). With a wide source and an unmeasured right pane, RightColumnPlacement.decide chose splitBelow. The width now comes from LayoutRootView.restingContentWidth(of:) — the frame the pane rests at in the geometry its screen's springs head for — so links and agent opens both read the final widths. The new read is an O(1) lookup into geometry computed on layout sync, adding no layout pass and no wait.

  • LinkOpener.width(of:) reads restingContentWidth(of:) instead of the pane view's bounds.
  • LayoutRootView.restingContentWidth(of:) returns nil for a pane no screen view lays out, as before.
  • Adds RightColumnAfterSplitTests.anOpenRightAfterASplitReadsThePanesFinalWidths, which drives the layout clock so the split's spring stays at its first frame on every host.

Written for commit 3af8e0a. Summary will update on new commits.

View guided diff Turn on auto-fix

…l widths

Review of #29787 at 2986104: LinkOpener.width(of:) reads a pane's view bounds. While a split's
spring runs, the source still shows its width from before the split and the new pane a zero-width
sliver, so in a window narrower than twice the chat floor a link (or an agent's side open through
rightColumn(beside:)) opened right after the split went to a split below the terminal-only right
pane instead of a tab in it.

The test is the layout's clock (driver.isManual), so the spring stays at its first frame and the
race is the same on every host. Red first: fails at this commit.
… path did not compile in the macro)

Fleet job cc5d395258652518fd0cb650 at abe70fc: RightColumnAfterSplitTests.swift:55 'call can throw,
but it is not marked with try' inside the #require expansion. Same assertion, no behavior change.
…ir view bounds

LinkOpener.width(of:), the one width every open on the right reads (links through target(from:),
agent side chats, subagents and beside moves through rightColumn(beside:)), read the pane view's
bounds. Right after a split the source still shows its width from before the split until the spring
lands, and the new pane starts as a zero-width sliver (nil, no floor). In a window narrower than
twice the chat floor a link or agent open right after a split went to a split below the
terminal-only right pane instead of a tab in it.

It now reads LayoutRootView.restingContentWidth(of:): the pane's frame in the geometry its screen's
springs head for (the same geometry the right column is read from), less the pane padding. An O(1)
lookup into geometry computed when the layout syncs; no layout pass, no wait.
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

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

Review 3af8e0a: 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=pr30010 -f ref=fix/pane-width-final-layout -f platform=macos -f backend_mode=local

Default changes

This pull request changes 1 default at 3af8e0a05961 (what everyone gets without configuring anything):

  • tunable-source.CmuxNextApp/Links/LinkOpener.swift:width: "88921620fb240be6cd49261b3f1ba372c48fc8fb46a938b57600c3fcb01cf8a7" -> "c3a4ef8486c3f4160d63b8d8e21842e9f1f21f606adb3bb6cce68f4a0783887e"

Signed off on this commit by review (independent review subagent, Opus 5.5 claude-opus-5-5, read-only).

Merge receipt: merged as 37a515e1d13f with these defaults changed, signed off by review (independent review subagent, Opus 5.5 claude-opus-5-5, read-only).

Regenerate the snapshot with python3 scripts/ci/defaults_guard.py snapshot.

Shipped in:

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent review subagent, Opus 5.5 claude-opus-5-5, read-only): SHIP at 3af8e0a

The reviewer confirmed:

  • restingContentWidth reads baseGeometry, the springs' target (ScreenContentView.swift:18). reconcile sets it before any spring starts (:212), and the quick split path syncs it in the same turn (PaneHandlers.swift:142, then WorkspaceContentController.swift:283,290).
  • No layout pass is forced, there is no wait and no I/O, and nothing per frame calls it.
  • Parked and off-screen workspaces read their last layout, as the old bounds did, so the column and the widths still agree. The only real change is the one this PR wants: a new pane in a sliver frame now reads its final width and not nil.
  • Every entry point uses the one source: openWeb, openFile, openFolder, ChatColumnPlacement.openTool, AgentTabStore.rightColumn and AgentBesidePlacement.
  • The red-to-green change comes only from the fix: from ade9546b to the head, only LinkOpener.swift and LayoutRootView.swift change.

Low notes, not blocking:

  • model.screen(containing:) is O(panes) per call, so the PR body's "O(1)" is not quite right.
  • The test comment says about 880 pt, but the red run measured 720 pt.
  • The claim that ClickedLinkFocusTests :75 and :149 pass because of this fix is not proven: the base and head runs were on different hosts.

Owner: LayoutRootView*.swift is in the Layout motion row (PL-motion-core). The change adds one read-only accessor, and the motion contract is the same.

Fleet: red 2f3bc17c13cb (3 failures), green a6ce3d5aa1df (7 placement suites). The remaining ClickedLinkFocusTests :256 (fixed by #29629) and :285, and ChatDockToolPlacementTests, fail the same way at the base; they are separate base reds, and an agent is on them.

@lawrencecchen
lawrencecchen marked this pull request as ready for review November 1, 2026 02:47
@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 1, 2026

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 3 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/RightColumnAfterSplitTests.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/RightColumnAfterSplitTests.swift:66">
P2: This does not verify agent placement, only the measurement snapshot and pure policy decision. Drive one real side-chat or subagent open after `splitRight` and assert its tab lands in `right`; otherwise regressions in `openOnRight` or the agent callbacks can pass this test.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

"the source read \(String(describing: LinkOpener.width(of: pane))) pt after the split, its width before it")

// Agent opens (AgentSidePanes, AgentSubagentPanes, AgentBesidePlacement) measure through `rightColumn`.
let measured = services.agentTabs.rightColumn(sourceTab)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This does not verify agent placement, only the measurement snapshot and pure policy decision. Drive one real side-chat or subagent open after splitRight and assert its tab lands in right; otherwise regressions in openOnRight or the agent callbacks can pass this test.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/RightColumnAfterSplitTests.swift, line 66:

<comment>This does not verify agent placement, only the measurement snapshot and pure policy decision. Drive one real side-chat or subagent open after `splitRight` and assert its tab lands in `right`; otherwise regressions in `openOnRight` or the agent callbacks can pass this test.</comment>

<file context>
@@ -0,0 +1,106 @@
+                    "the source read \(String(describing: LinkOpener.width(of: pane))) pt after the split, its width before it")
+
+            // Agent opens (AgentSidePanes, AgentSubagentPanes, AgentBesidePlacement) measure through `rightColumn`.
+            let measured = services.agentTabs.rightColumn(sourceTab)
+            #expect(RightColumnPlacement.decide(measured) == .tab(right.layoutPaneID.rawValue),
+                    "an agent's open right after the split: \(measured)")
</file context>

@blacksmith-sh

blacksmith-sh Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Found 2 test failures on Blacksmith runners:

Failures

Test View Logs
test_no_runner_picker_returns_a_hosted_mac (main.NoHostedMacInNightlies.test_no_run
ner_picker_returns_a_hosted_mac)/
test_no_runner_picker_returns_a_hosted_mac (main.NoHostedMacInNightlies.test_no_run
ner_picker_returns_a_hosted_mac)
View Logs
test_the_repository_highlights_validate (main.DigestTests.test_the_repository_highl
ights_validate)/
test_the_repository_highlights_validate (main.DigestTests.test_the_repository_highl
ights_validate)
View Logs

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

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 3af8e0a0 Deployed Nov 1, 2026 by lawrencecchen via namespace autofix #10605
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