Repository navigation
cmux NEXT: anything opened on the right goes to the lower pane when the right already holds something - #28044
teamleaderleo wants to merge 17 commits into
Conversation
Leo, 2026-10-28: "But I don't know if you want to do more splits versus just adding more to the top tab or space pane, the top pane. I mean the right top pane, and then you split to the bottom once so that you get the three items. But you want four items. I don't know what the fourth item should go." Transcript subagent and Task cards (chat.subagentPane, AgentSubagentPanes) stacked a new split below the newest subagent pane for every subagent, with no cap and no width floor. They now follow leo-workspace design/pane-placement.md: an open subagent's tab is shown; the first opens in a split right of the chat, the second in one split below it, and the third and later are tabs in the newest subagent's pane, never another split. No split leaves the chat under 480 pt or the right column under 420 pt; that open is a tab instead. An unfocused tab open leaves focus where it is. The decision is one function, AgentSubagentPanes.placement(chat:hooks:), so it can fold into #26582's RightPanePlacement later. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
…cused subagent tab is shown in its pane Review of #27789 at 1eed675 (Codex, BLOCK): - openSubagent passed main: for every split, so in main pane layout the second subagent's split below became another tab in the right column. Only the first, chat-relative split goes through main pane layout now. - An unfocused .tab open was created unselected, so the new subagent was hidden behind the one already showing. It is selected in its subagent pane without moving keyboard focus (selectWhenReported); in the chat's own pane (a narrow window) it waits in the strip so the chat stays shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
… public API) Fleet compile at b17ce6d: 'cannot be declared public because its type uses an internal type' for the width floors and the width hook, under InternalImportsByDefault. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
…he right already holds something Leo, 2026-10-30: "subagent stuff... or anything we expect to open on "the right"... if there's already a thing on the right, there should be logic for just putting it on the lower pane of the right. that kinda split." One rule, RightColumnPlacement.decide (CmuxNextAgentPane), now places every open on the right of a pane, from the right column the window shows (RightColumn.measure / RightColumn.panes): - nothing on the right: a split right of the source; - one pane on the right: a split below it (it keeps what it shows); - two or more: a tab in the lowest pane, never another split; - no split leaves the chat under 480 pt or the right column under 420 pt. Closing the lower pane's last tab removes the pane, so the split collapses and the next open splits below again. Paths routed through it: - transcript subagent / Task cards (AgentSubagentPanes, #27789's function, now shared and read from the window's geometry); - side chats, /btw, Session Info sessions and transcript session cards (AgentSidePanes; chat.sideChat and chat.offer.open); - links opened to the right and chats opened from Needs you (LinkOpener / LinkOpenPlacement gain .splitBelow, for a source that takes tabs). A chat that is its screen's main pane keeps main pane layout's own rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
|
Found 6 test failures on Blacksmith runners: Failures
|
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) web and size checks: green |
ux-owner, 2026-10-30, on Leo's "anything we expect to open on 'the right'": in main pane layout the strip column right of the docked chat is that right side. A chat there whose right side already holds a pane splits below it, then fills the lower pane with tabs (links through LinkOpenPlacement.inColumn, chat opens through openOnRight). With nothing on its right yet, main pane layout still makes the column (dock the chat, or a tab in the strip pane). Home's pinned dock is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
Review of #28044 at 20e433f (Codex, BLOCK, P1): RightColumn walked the column with PaneHandlers.neighbor, whose horizontal step picks the adjacent column's most recently focused pane, so a two-pane right column could read as one and the next open split again or targeted the wrong pane. RightColumnPlacement.column(beside:frames:gap:) now takes the panes whose left edge is nearest the source's right edge and that overlap its height, top to bottom, from the layout's navigation frames. The review's other two P1s (main pane layout's chat opens and links skipped the rule) were fixed at 36862fa. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
…the tab Review of #28044 at 34122a1 (Codex, BLOCK, P1): with no pane on its right, LinkOpenPlacement returned .splitRight without the chat floor, so a 900-959 pt source split to under 480 pt. RightColumnPlacement.decide now judges that split too: measured too narrow, the tab opens in the source pane. A new column (layout.linkPlacement new-column) adds width to the strip, so it keeps opening one; an unmeasured source keeps the old behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
…s pane through the real placement A capture of #28044 (4154db0 vs 34122a1) never reached its scene: a tray line's chat.subagentPane answered opened:false for the fixture's subagents, because a showcase chat's page never persists the mock daemon's session, so the model has none, and the page showed the subagent in the chat instead. `debug.agent_pane {action: open_subagent, index, focus}` (DEBUG and bench builds only, DebugAgentPaneSubagent) calls the same header hook chat.subagentPane calls (AgentTabStore.openSubagent, then AgentSubagentPanes and openBeside place the pane), with the page's session when the model has none. Both capture states use it instead of tray clicks. An unfocused open keeps focus on the chat, so their focusLeft steps go. Also formats the fixture test's import (fast gates' web lint). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
…pane Conflicts: #28044 replaces #27789's per-tracker placement with the shared RightColumnPlacement, so #28044's side wins in AgentSubagentPanes, AgentTabs, AgentTabs+Header and the placement tests, keeping #27789's two later fixes: - selectInPane uses selectWhenReportedKeepingFocus (review at 28ad8d1); - main pane layout decides first when nothing is on the chat's right, including a chat too narrow to split (review at 84718cf): #28044's openOnRight already asks openInMainPane for .splitRight and .tab(nil). SubagentMainPaneRoutingTests (from #27789) feeds rightColumn instead of the removed paneWidth hook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
|
Review (codex-review-box/gpt-6-astra high, lane-place): BLOCK at bd2b918 BLOCK
Full diff reviewed and verified against the supplied tree. macOS tests could not run: this Linux box has no Swift toolchain. |
… open again is found anywhere in it Review of #28044 at bd2b918 (Codex, BLOCK, two P2s): - RightColumn read navigationFrames, which holds the active screen only. An open still queued when the person switched screens read the chat's screen as empty, so it opened in the chat's own pane or split again beside panes already there. LayoutRootView.navigationFrames(ofScreenContaining:) reads the screen holding the pane, active or not (a hidden screen view keeps its geometry), and RightColumn.panes uses it. - openWeb looked for a tab of the same page only in the pane this open would use. With A (upper) and B (lower) open, A again targets the lower pane and opened a second A. LinkOpenPlacement.reusePanes lists that pane first, then every pane of the right column, never the source. Tests: RightColumnScreenTests (the chat's column survives a switch to another screen; red with the active screen's frames) and LinkOpenPlacementTests.aLinkOpenAgainIsFoundAnywhereInTheRightColumn (A, B, A: A is selected in the upper pane). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
|
Review (codex-review-box/gpt-6-astra high, lane-place): BLOCK at 1cfd9d1 BLOCK
|
…the active one Review of #28044 at 1cfd9d1 (Codex, BLOCK, P1): LinkOpener.target measured the right column on the chat's own screen but found the pane on its right with PaneHandlers.neighbor, which read the active screen's frames. A link opened from a chat on a screen the person left found no neighbour and split again. PaneHandlers.neighbor now reads navigationFrames(ofScreenContaining: pane), as do the two PanePlacementRouting sites (New Pane auto layout and tiling placement), the only other places that paired a pane's own screen layout with the active screen's frames. On the active screen nothing changes. RightColumnScreenTests covers the right-hand neighbour on a screen the person left. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
…link reuses nothing on the right Review of #28044 at c15e0d8 (Codex, BLOCK, two P2s): - Two links opened before either's move landed both read a one-pane right column and both split below it: three panes on the right. A lower split under way is reserved (RightColumn.reserveBelow, released when its move lands or fails, 30 s backstop): until then the column reads as full, so an open meanwhile is a tab, in the right pane that holds the moving tab (RightColumn.realPane). LinkOpener and the agent split-below path (openOnRight) both reserve, so a link and a subagent can't race either. Subagent opens were already one at a time per tracker. - LinkOpenPlacement.reusePanes reused a tab on the right even when the open lands in the source (Same Pane, or a source too narrow to split). It returns nothing then. Tests: RightColumnPendingSplitTests (a second open during a split is a tab in the right pane; overlapping reservations hold until the last ends) and LinkOpenPlacementTests (Same Pane reuses nothing on the right). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
…'s stand-in Review of #28044 at a4ae195 (Codex, BLOCK, P2): while a lower split's move was under way, RightColumn.panes listed a `pending-below:` stand-in for the pane still moving in, and LinkOpener.target passed it on. openFolder looked it up, found no pane and opened its terminal in the source over the chat; DurableTabLink's fallback lost its destination the same way. The stand-in now never leaves RightColumn: panes(beside:in:) maps it back to the right pane (realPane) before returning, so a pending split reads as its right pane listed twice. RightColumnPlacement still sees a full column, and the tab goes to the pane that holds the moving tab. Every caller (LinkOpener.target for links, folders, DurableTabLink, PaneBrowserTabOpener and New Tab; RightColumn.measure for agent opens) gets real panes only, so the per-call realPane mapping in LinkOpener.open and locatePane goes. RightColumnPendingTargetTests: in a real two-pane window with a lower split pending, LinkOpener.target names the real right pane (.pane, not a split below and not the stand-in), DurableTabLink's existingPane resolves, and every pane the column and its measure name exists. The test checks the placement answer that openFolder and the fallback read, not a daemon-made split. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
…rce pane Five reviews of #28044 each found two opens that read one right column before either landed, in a path the last fix missed (links at c15e0d8, folders at e954bd8), or a pending split's stand-in id leaking to a caller (a4ae195). The reservation and its stand-in pane are gone. In their place, one entry point: RightColumn.serially(beside:_:) runs the opens beside one source pane one at a time. Each decides its place from the column as the open before it left it, and holds the turn until its move has landed or failed (30 s backstop). Placement only ever sees real panes. Every opener on the right goes through it: - LinkOpener.open: links, files, browser tabs, New Tab picks, the diff page, Needs you (openRight). It decides inside its turn (place), and moved/moveBeside/dockChat end the turn when their move lands. - LinkOpener.openFolder: its turn too. A split's terminal is made in the pane it splits and moved there, as a New Tab page's terminal already was, instead of a splitRight/splitDown action the turn could not see land. New Column still makes a split, as before. - AgentTabStore.openOnRight: subagents, side chats, /btw, Session Info sessions, transcript session cards. It decides again in its turn (the trackers' decision is from when they asked), passes that decision to `created`, and ends the turn in `finished`. Wired in AgentTabs.serially. Tests: RightColumnTurnTests (the turn; every pair of the seven opener kinds from an empty column and from one pane on the right, both asked before either lands, never leaves more than two panes; all of them at once fill one column) and RightColumnEntryPointTests (a LinkOpener open and an AgentTabStore open each wait for the open under way, then decide). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
PaneHandlers.neighbor and PanePlacementRouting's autoLayoutTarget and route now read the frames of the pane's own screen (navigationFrames(ofScreenContaining:)); the defaults guard hashes them as tunable sources. Nothing a person gets by default changes on the active screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
Fleet run of #28044 at 8e52036 (scratch #28392): - SideOpenStripTests failed: its pane has no window, so its width was 0, and the narrow-chat floor (5ade38f) took 0 pt for too narrow to split, opening the link in place. RightColumn.width(of:) answers nil for a pane with no width yet; LinkOpener.target and RightColumn.measure use it, so an unmeasured pane sets no floor, as before the floor. - RightColumnScreenTests required the chat's screen to be hidden after the switch, but its fade ends on later frames a headless window never draws. It requires the screen to be inactive instead; a hidden screen view keeps the same geometry, so the lookup reads it either way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
…e is no width Fleet run of #28044 at caf8e69 (scratch #28392): SideOpenStripTests still opened its link in place. A PaneContentView starts at a 400 pt placeholder frame, so a pane not yet laid out read as 400 pt, under the 480 pt chat floor. caf8e69 only treated a 0 pt width as unmeasured; RightColumn.width(of:) now answers nil for any pane that is not in a window, then for a 0 pt one. SideOpenStripTests.aPaneNotInAWindowIsUnmeasured covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7
…(agents), plus capture states (#28213) * debug(next): the subagents fixture can start more than two subagents (agents) `debug.agent_pane seed_rows` with `fixture: "subagents"` takes `agents` (2 by default, up to 8): past the reviewer and the auditor, helpers that stay running through every step. A capture can then open 3 or more subagents' panes and record where each lands (leo-workspace design/pane-placement.md; cmux#27789 and #28044). Leo, 2026-10-28: "I mean the right top pane, and then you split to the bottom once so that you get the three items. But you want four items." Debug-only (debug.agent_pane, DEBUG builds); with `agents` unset the fixture is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * media(next): capture states for subagents in the right column (#27789, #28044) subagents-four-panes.json opens four fixture subagents from the chat's tray; subagents-beside-right-pane.json opens two beside a terminal already on the chat's right. Both drive the tray by CSS selector, refocusing the chat between opens, so no step depends on label text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * test(next): the fixture test's count parameter no longer shadows its agents list Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * debug(next): debug.agent_pane open_subagent opens a fixture subagent's pane through the real placement A capture of #28044 (4154db0 vs 34122a1) never reached its scene: a tray line's chat.subagentPane answered opened:false for the fixture's subagents, because a showcase chat's page never persists the mock daemon's session, so the model has none, and the page showed the subagent in the chat instead. `debug.agent_pane {action: open_subagent, index, focus}` (DEBUG and bench builds only, DebugAgentPaneSubagent) calls the same header hook chat.subagentPane calls (AgentTabStore.openSubagent, then AgentSubagentPanes and openBeside place the pane), with the page's session when the model has none. Both capture states use it instead of tray clicks. An unfocused open keeps focus on the chat, so their focusLeft steps go. Also formats the fixture test's import (fast gates' web lint). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * test(next): name the Settings JSONValue in the subagent fixture test The base merge brought a second JSONValue into scope: CmuxNextDaemon's beside CmuxNextSettings'. That made the test's type ambiguous (fleet test-compile at 2dbdebd). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
Owned by an agent session · took over from an earlier session · session |
|
This PR's change is carried forward in #28989, which takes this branch's commits and fixes the review findings: LinkOpener now reads the right column from the source pane's own workspace (not the active screen), pane keys are scoped to the chat's workspace, an open shown in place ends its turn, and a refusal ends its turn right away. #28989 has an independent SHIP at its head and is merging now. This PR is yours to close once that lands. |
…lower pane when the right already holds something (#28989) * fix(next): subagents fill one split below the first, then open as tabs Leo, 2026-10-28: "But I don't know if you want to do more splits versus just adding more to the top tab or space pane, the top pane. I mean the right top pane, and then you split to the bottom once so that you get the three items. But you want four items. I don't know what the fourth item should go." Transcript subagent and Task cards (chat.subagentPane, AgentSubagentPanes) stacked a new split below the newest subagent pane for every subagent, with no cap and no width floor. They now follow leo-workspace design/pane-placement.md: an open subagent's tab is shown; the first opens in a split right of the chat, the second in one split below it, and the third and later are tabs in the newest subagent's pane, never another split. No split leaves the chat under 480 pt or the right column under 420 pt; that open is a tab instead. An unfocused tab open leaves focus where it is. The decision is one function, AgentSubagentPanes.placement(chat:hooks:), so it can fold into #26582's RightPanePlacement later. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): the lower subagent split splits the subagent pane; an unfocused subagent tab is shown in its pane Review of #27789 at 1eed675 (Codex, BLOCK): - openSubagent passed main: for every split, so in main pane layout the second subagent's split below became another tab in the right column. Only the first, chat-relative split goes through main pane layout now. - An unfocused .tab open was created unselected, so the new subagent was hidden behind the one already showing. It is selected in its subagent pane without moving keyboard focus (selectWhenReported); in the chat's own pane (a narrow window) it waits in the strip so the chat stays shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): AgentSubagentPanes imports AppKit publicly (CGFloat in its public API) Fleet compile at b17ce6d: 'cannot be declared public because its type uses an internal type' for the width floors and the width hook, under InternalImportsByDefault. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * cmux NEXT: anything opened on the right goes to the lower pane when the right already holds something Leo, 2026-10-30: "subagent stuff... or anything we expect to open on "the right"... if there's already a thing on the right, there should be logic for just putting it on the lower pane of the right. that kinda split." One rule, RightColumnPlacement.decide (CmuxNextAgentPane), now places every open on the right of a pane, from the right column the window shows (RightColumn.measure / RightColumn.panes): - nothing on the right: a split right of the source; - one pane on the right: a split below it (it keeps what it shows); - two or more: a tab in the lowest pane, never another split; - no split leaves the chat under 480 pt or the right column under 420 pt. Closing the lower pane's last tab removes the pane, so the split collapses and the next open splits below again. Paths routed through it: - transcript subagent / Task cards (AgentSubagentPanes, #27789's function, now shared and read from the window's geometry); - side chats, /btw, Session Info sessions and transcript session cards (AgentSidePanes; chat.sideChat and chat.offer.open); - links opened to the right and chats opened from Needs you (LinkOpener / LinkOpenPlacement gain .splitBelow, for a source that takes tabs). A chat that is its screen's main pane keeps main pane layout's own rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * cmux NEXT: main pane layout's strip column gets the same lower split ux-owner, 2026-10-30, on Leo's "anything we expect to open on 'the right'": in main pane layout the strip column right of the docked chat is that right side. A chat there whose right side already holds a pane splits below it, then fills the lower pane with tabs (links through LinkOpenPlacement.inColumn, chat opens through openOnRight). With nothing on its right yet, main pane layout still makes the column (dock the chat, or a tab in the strip pane). Home's pinned dock is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): read the right column from pane frames, not focus recency Review of #28044 at 20e433f (Codex, BLOCK, P1): RightColumn walked the column with PaneHandlers.neighbor, whose horizontal step picks the adjacent column's most recently focused pane, so a two-pane right column could read as one and the next open split again or targeted the wrong pane. RightColumnPlacement.column(beside:frames:gap:) now takes the panes whose left edge is nearest the source's right edge and that overlap its height, top to bottom, from the layout's navigation frames. The review's other two P1s (main pane layout's chat opens and links skipped the rule) were fixed at 36862fa. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): a link from a narrow pane with nothing on its right keeps the tab Review of #28044 at 34122a1 (Codex, BLOCK, P1): with no pane on its right, LinkOpenPlacement returned .splitRight without the chat floor, so a 900-959 pt source split to under 480 pt. RightColumnPlacement.decide now judges that split too: measured too narrow, the tab opens in the source pane. A new column (layout.linkPlacement new-column) adds width to the strip, so it keeps opening one; an unmeasured source keeps the old behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): the right column reads the source's own screen, and a link open again is found anywhere in it Review of #28044 at bd2b918 (Codex, BLOCK, two P2s): - RightColumn read navigationFrames, which holds the active screen only. An open still queued when the person switched screens read the chat's screen as empty, so it opened in the chat's own pane or split again beside panes already there. LayoutRootView.navigationFrames(ofScreenContaining:) reads the screen holding the pane, active or not (a hidden screen view keeps its geometry), and RightColumn.panes uses it. - openWeb looked for a tab of the same page only in the pane this open would use. With A (upper) and B (lower) open, A again targets the lower pane and opened a second A. LinkOpenPlacement.reusePanes lists that pane first, then every pane of the right column, never the source. Tests: RightColumnScreenTests (the chat's column survives a switch to another screen; red with the active screen's frames) and LinkOpenPlacementTests.aLinkOpenAgainIsFoundAnywhereInTheRightColumn (A, B, A: A is selected in the upper pane). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): every placement reads the screen that holds the pane, not the active one Review of #28044 at 1cfd9d1 (Codex, BLOCK, P1): LinkOpener.target measured the right column on the chat's own screen but found the pane on its right with PaneHandlers.neighbor, which read the active screen's frames. A link opened from a chat on a screen the person left found no neighbour and split again. PaneHandlers.neighbor now reads navigationFrames(ofScreenContaining: pane), as do the two PanePlacementRouting sites (New Pane auto layout and tiling placement), the only other places that paired a pane's own screen layout with the active screen's frames. On the active screen nothing changes. RightColumnScreenTests covers the right-hand neighbour on a screen the person left. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): an open during a lower split's move is a tab; a Same Pane link reuses nothing on the right Review of #28044 at c15e0d8 (Codex, BLOCK, two P2s): - Two links opened before either's move landed both read a one-pane right column and both split below it: three panes on the right. A lower split under way is reserved (RightColumn.reserveBelow, released when its move lands or fails, 30 s backstop): until then the column reads as full, so an open meanwhile is a tab, in the right pane that holds the moving tab (RightColumn.realPane). LinkOpener and the agent split-below path (openOnRight) both reserve, so a link and a subagent can't race either. Subagent opens were already one at a time per tracker. - LinkOpenPlacement.reusePanes reused a tab on the right even when the open lands in the source (Same Pane, or a source too narrow to split). It returns nothing then. Tests: RightColumnPendingSplitTests (a second open during a split is a tab in the right pane; overlapping reservations hold until the last ends) and LinkOpenPlacementTests (Same Pane reuses nothing on the right). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): placement hands out real panes only, never a pending split's stand-in Review of #28044 at a4ae195 (Codex, BLOCK, P2): while a lower split's move was under way, RightColumn.panes listed a `pending-below:` stand-in for the pane still moving in, and LinkOpener.target passed it on. openFolder looked it up, found no pane and opened its terminal in the source over the chat; DurableTabLink's fallback lost its destination the same way. The stand-in now never leaves RightColumn: panes(beside:in:) maps it back to the right pane (realPane) before returning, so a pending split reads as its right pane listed twice. RightColumnPlacement still sees a full column, and the tab goes to the pane that holds the moving tab. Every caller (LinkOpener.target for links, folders, DurableTabLink, PaneBrowserTabOpener and New Tab; RightColumn.measure for agent opens) gets real panes only, so the per-call realPane mapping in LinkOpener.open and locatePane goes. RightColumnPendingTargetTests: in a real two-pane window with a lower split pending, LinkOpener.target names the real right pane (.pane, not a split below and not the stand-in), DurableTabLink's existingPane resolves, and every pane the column and its measure name exists. The test checks the placement answer that openFolder and the fallback read, not a daemon-made split. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * refactor(next): every open on the right takes its turn beside its source pane Five reviews of #28044 each found two opens that read one right column before either landed, in a path the last fix missed (links at c15e0d8, folders at e954bd8), or a pending split's stand-in id leaking to a caller (a4ae195). The reservation and its stand-in pane are gone. In their place, one entry point: RightColumn.serially(beside:_:) runs the opens beside one source pane one at a time. Each decides its place from the column as the open before it left it, and holds the turn until its move has landed or failed (30 s backstop). Placement only ever sees real panes. Every opener on the right goes through it: - LinkOpener.open: links, files, browser tabs, New Tab picks, the diff page, Needs you (openRight). It decides inside its turn (place), and moved/moveBeside/dockChat end the turn when their move lands. - LinkOpener.openFolder: its turn too. A split's terminal is made in the pane it splits and moved there, as a New Tab page's terminal already was, instead of a splitRight/splitDown action the turn could not see land. New Column still makes a split, as before. - AgentTabStore.openOnRight: subagents, side chats, /btw, Session Info sessions, transcript session cards. It decides again in its turn (the trackers' decision is from when they asked), passes that decision to `created`, and ends the turn in `finished`. Wired in AgentTabs.serially. Tests: RightColumnTurnTests (the turn; every pair of the seven opener kinds from an empty column and from one pane on the right, both asked before either lands, never leaves more than two panes; all of them at once fill one column) and RightColumnEntryPointTests (a LinkOpener open and an AgentTabStore open each wait for the open under way, then decide). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * chore(next): refresh the defaults snapshot for the screen-frames change PaneHandlers.neighbor and PanePlacementRouting's autoLayoutTarget and route now read the frames of the pane's own screen (navigationFrames(ofScreenContaining:)); the defaults guard hashes them as tunable sources. Nothing a person gets by default changes on the active screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): a pane with no width is unmeasured, not too narrow to split Fleet run of #28044 at 8e52036 (scratch #28392): - SideOpenStripTests failed: its pane has no window, so its width was 0, and the narrow-chat floor (5ade38f) took 0 pt for too narrow to split, opening the link in place. RightColumn.width(of:) answers nil for a pane with no width yet; LinkOpener.target and RightColumn.measure use it, so an unmeasured pane sets no floor, as before the floor. - RightColumnScreenTests required the chat's screen to be hidden after the switch, but its fade ends on later frames a headless window never draws. It requires the screen to be inactive instead; a hidden screen view keeps the same geometry, so the lookup reads it either way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): a pane not in a window is unmeasured: its placeholder frame is no width Fleet run of #28044 at caf8e69 (scratch #28392): SideOpenStripTests still opened its link in place. A PaneContentView starts at a 400 pt placeholder frame, so a pane not yet laid out read as 400 pt, under the 480 pt chat floor. caf8e69 only treated a 0 pt width as unmeasured; RightColumn.width(of:) now answers nil for any pane that is not in a window, then for a 0 pt one. SideOpenStripTests.aPaneNotInAWindowIsUnmeasured covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): a link's right side falls back to the measured column's top pane Review of #28044 at 1cfd9d1 (P1): right came from the active screen while column came from the source's. c15e0d8 already moved PaneHandlers.neighbor to the source screen's frames; this makes an occupied column never read as empty, should the neighbour rule still find no pane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64 * refactor(next): right-column turns are an instance in CmuxNextAgentPane, not App statics RightColumn.serially kept its queue in two static vars of a new App file (the arch ratchet's global_mutable_statics row, and a new App file for the app-size rule). The queue is now RightColumnTurns<Source> in CmuxNextAgentPane, next to RightColumnPlacement; the app's LinkOpener owns the one instance, and AgentTabStore's serially hook reaches it through services.linkOpener. The geometry helpers (measure, width, panes) move into LinkOpener.swift, their only App user besides the wiring. No behaviour change; RightColumnTurnTests and RightColumnEntryPointTests use an instance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64 * fix(next): a right-column pane is looked up only in the chat's own workspace Review of the #28044 carry at d495c86 (P1): locatePane took the first pane with the column's key across every window. A layout pane id without a resource id is `pane:<handle>`, which two daemons or workspaces can share, so a split below or a tab could land in another workspace. The lookup now starts from the chat tab's own pane, the same workspace rightColumn measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64 * fix(next): right-side opens shown in place, folder terminals and deferred refusals end cleanly Review of the #28044 carry at 1eb458c: - P1: a file a tab already shows is selected where it is and never reports to `placed`, so its turn held the source pane until the 30 s backstop and the next open waited. FilePageService now says so (`shownInPlace`, through ViewerService and FilePageOpener), LinkOpener ends the turn (`openedInPlace`, shared `ended` for split, besideDock and dockChat opens). RightColumnEntryPointTests.anOpenShownWhereItAlreadyWasEndsItsTurn covers it. - P2: a folder's terminal is made with `placementOwner` when it is moved, so it never shows in the source pane first (as base's split path and a link's browser tab do). - P2: an agent open that waited its turn and was then refused calls `finished(false)`; its caller had already heard true, so the subagent or side-chat tracker no longer stays "opening". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64 * fix(next): openFile builds its in-place hook outside the call The fleet compile at e603a67 failed: "failed to produce diagnostic for expression" on the ternary closure argument. The hook is now a typed local. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64 * arch-ratchet: reviewed app_lines exception for the right-column wiring (#28989) The pure rule and turn queue live in CmuxNextAgentPane; the +232 App lines are LinkOpener and AgentTabStore glue over App types. Receipt: codex gpt-6-astra SHIP at e603a67. Expires 2026-11-30. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
…-pane Conflicts in LinkOpener.swift and AgentTabs+Header.swift: #28989 (the carry of #28044) now places everything a chat opens in its right column (a split below its one pane, else its lower pane), Leo's later ask: "go split instead of just adding a third column". That supersedes this PR's 'a tab in the existing right pane' path, so both files take the base's version, and the fromChat flag, existingRight and rightPaneTab go. The rest of this PR (New Tab and dock picks beside the chat, the typed-command rule, the link fixture) is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-8451c317c43e92348de8
* fix(next): subagents fill one split below the first, then open as tabs Leo, 2026-10-28: "But I don't know if you want to do more splits versus just adding more to the top tab or space pane, the top pane. I mean the right top pane, and then you split to the bottom once so that you get the three items. But you want four items. I don't know what the fourth item should go." Transcript subagent and Task cards (chat.subagentPane, AgentSubagentPanes) stacked a new split below the newest subagent pane for every subagent, with no cap and no width floor. They now follow leo-workspace design/pane-placement.md: an open subagent's tab is shown; the first opens in a split right of the chat, the second in one split below it, and the third and later are tabs in the newest subagent's pane, never another split. No split leaves the chat under 480 pt or the right column under 420 pt; that open is a tab instead. An unfocused tab open leaves focus where it is. The decision is one function, AgentSubagentPanes.placement(chat:hooks:), so it can fold into #26582's RightPanePlacement later. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): the lower subagent split splits the subagent pane; an unfocused subagent tab is shown in its pane Review of #27789 at 1eed675 (Codex, BLOCK): - openSubagent passed main: for every split, so in main pane layout the second subagent's split below became another tab in the right column. Only the first, chat-relative split goes through main pane layout now. - An unfocused .tab open was created unselected, so the new subagent was hidden behind the one already showing. It is selected in its subagent pane without moving keyboard focus (selectWhenReported); in the chat's own pane (a narrow window) it waits in the strip so the chat stays shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): AgentSubagentPanes imports AppKit publicly (CGFloat in its public API) Fleet compile at b17ce6d: 'cannot be declared public because its type uses an internal type' for the width floors and the width hook, under InternalImportsByDefault. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * cmux NEXT: anything opened on the right goes to the lower pane when the right already holds something Leo, 2026-10-30: "subagent stuff... or anything we expect to open on "the right"... if there's already a thing on the right, there should be logic for just putting it on the lower pane of the right. that kinda split." One rule, RightColumnPlacement.decide (CmuxNextAgentPane), now places every open on the right of a pane, from the right column the window shows (RightColumn.measure / RightColumn.panes): - nothing on the right: a split right of the source; - one pane on the right: a split below it (it keeps what it shows); - two or more: a tab in the lowest pane, never another split; - no split leaves the chat under 480 pt or the right column under 420 pt. Closing the lower pane's last tab removes the pane, so the split collapses and the next open splits below again. Paths routed through it: - transcript subagent / Task cards (AgentSubagentPanes, #27789's function, now shared and read from the window's geometry); - side chats, /btw, Session Info sessions and transcript session cards (AgentSidePanes; chat.sideChat and chat.offer.open); - links opened to the right and chats opened from Needs you (LinkOpener / LinkOpenPlacement gain .splitBelow, for a source that takes tabs). A chat that is its screen's main pane keeps main pane layout's own rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * cmux NEXT: main pane layout's strip column gets the same lower split ux-owner, 2026-10-30, on Leo's "anything we expect to open on 'the right'": in main pane layout the strip column right of the docked chat is that right side. A chat there whose right side already holds a pane splits below it, then fills the lower pane with tabs (links through LinkOpenPlacement.inColumn, chat opens through openOnRight). With nothing on its right yet, main pane layout still makes the column (dock the chat, or a tab in the strip pane). Home's pinned dock is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): read the right column from pane frames, not focus recency Review of #28044 at 20e433f (Codex, BLOCK, P1): RightColumn walked the column with PaneHandlers.neighbor, whose horizontal step picks the adjacent column's most recently focused pane, so a two-pane right column could read as one and the next open split again or targeted the wrong pane. RightColumnPlacement.column(beside:frames:gap:) now takes the panes whose left edge is nearest the source's right edge and that overlap its height, top to bottom, from the layout's navigation frames. The review's other two P1s (main pane layout's chat opens and links skipped the rule) were fixed at 36862fa. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): a link from a narrow pane with nothing on its right keeps the tab Review of #28044 at 34122a1 (Codex, BLOCK, P1): with no pane on its right, LinkOpenPlacement returned .splitRight without the chat floor, so a 900-959 pt source split to under 480 pt. RightColumnPlacement.decide now judges that split too: measured too narrow, the tab opens in the source pane. A new column (layout.linkPlacement new-column) adds width to the strip, so it keeps opening one; an unmeasured source keeps the old behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): the right column reads the source's own screen, and a link open again is found anywhere in it Review of #28044 at bd2b918 (Codex, BLOCK, two P2s): - RightColumn read navigationFrames, which holds the active screen only. An open still queued when the person switched screens read the chat's screen as empty, so it opened in the chat's own pane or split again beside panes already there. LayoutRootView.navigationFrames(ofScreenContaining:) reads the screen holding the pane, active or not (a hidden screen view keeps its geometry), and RightColumn.panes uses it. - openWeb looked for a tab of the same page only in the pane this open would use. With A (upper) and B (lower) open, A again targets the lower pane and opened a second A. LinkOpenPlacement.reusePanes lists that pane first, then every pane of the right column, never the source. Tests: RightColumnScreenTests (the chat's column survives a switch to another screen; red with the active screen's frames) and LinkOpenPlacementTests.aLinkOpenAgainIsFoundAnywhereInTheRightColumn (A, B, A: A is selected in the upper pane). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): every placement reads the screen that holds the pane, not the active one Review of #28044 at 1cfd9d1 (Codex, BLOCK, P1): LinkOpener.target measured the right column on the chat's own screen but found the pane on its right with PaneHandlers.neighbor, which read the active screen's frames. A link opened from a chat on a screen the person left found no neighbour and split again. PaneHandlers.neighbor now reads navigationFrames(ofScreenContaining: pane), as do the two PanePlacementRouting sites (New Pane auto layout and tiling placement), the only other places that paired a pane's own screen layout with the active screen's frames. On the active screen nothing changes. RightColumnScreenTests covers the right-hand neighbour on a screen the person left. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): an open during a lower split's move is a tab; a Same Pane link reuses nothing on the right Review of #28044 at c15e0d8 (Codex, BLOCK, two P2s): - Two links opened before either's move landed both read a one-pane right column and both split below it: three panes on the right. A lower split under way is reserved (RightColumn.reserveBelow, released when its move lands or fails, 30 s backstop): until then the column reads as full, so an open meanwhile is a tab, in the right pane that holds the moving tab (RightColumn.realPane). LinkOpener and the agent split-below path (openOnRight) both reserve, so a link and a subagent can't race either. Subagent opens were already one at a time per tracker. - LinkOpenPlacement.reusePanes reused a tab on the right even when the open lands in the source (Same Pane, or a source too narrow to split). It returns nothing then. Tests: RightColumnPendingSplitTests (a second open during a split is a tab in the right pane; overlapping reservations hold until the last ends) and LinkOpenPlacementTests (Same Pane reuses nothing on the right). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): placement hands out real panes only, never a pending split's stand-in Review of #28044 at a4ae195 (Codex, BLOCK, P2): while a lower split's move was under way, RightColumn.panes listed a `pending-below:` stand-in for the pane still moving in, and LinkOpener.target passed it on. openFolder looked it up, found no pane and opened its terminal in the source over the chat; DurableTabLink's fallback lost its destination the same way. The stand-in now never leaves RightColumn: panes(beside:in:) maps it back to the right pane (realPane) before returning, so a pending split reads as its right pane listed twice. RightColumnPlacement still sees a full column, and the tab goes to the pane that holds the moving tab. Every caller (LinkOpener.target for links, folders, DurableTabLink, PaneBrowserTabOpener and New Tab; RightColumn.measure for agent opens) gets real panes only, so the per-call realPane mapping in LinkOpener.open and locatePane goes. RightColumnPendingTargetTests: in a real two-pane window with a lower split pending, LinkOpener.target names the real right pane (.pane, not a split below and not the stand-in), DurableTabLink's existingPane resolves, and every pane the column and its measure name exists. The test checks the placement answer that openFolder and the fallback read, not a daemon-made split. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * refactor(next): every open on the right takes its turn beside its source pane Five reviews of #28044 each found two opens that read one right column before either landed, in a path the last fix missed (links at c15e0d8, folders at e954bd8), or a pending split's stand-in id leaking to a caller (a4ae195). The reservation and its stand-in pane are gone. In their place, one entry point: RightColumn.serially(beside:_:) runs the opens beside one source pane one at a time. Each decides its place from the column as the open before it left it, and holds the turn until its move has landed or failed (30 s backstop). Placement only ever sees real panes. Every opener on the right goes through it: - LinkOpener.open: links, files, browser tabs, New Tab picks, the diff page, Needs you (openRight). It decides inside its turn (place), and moved/moveBeside/dockChat end the turn when their move lands. - LinkOpener.openFolder: its turn too. A split's terminal is made in the pane it splits and moved there, as a New Tab page's terminal already was, instead of a splitRight/splitDown action the turn could not see land. New Column still makes a split, as before. - AgentTabStore.openOnRight: subagents, side chats, /btw, Session Info sessions, transcript session cards. It decides again in its turn (the trackers' decision is from when they asked), passes that decision to `created`, and ends the turn in `finished`. Wired in AgentTabs.serially. Tests: RightColumnTurnTests (the turn; every pair of the seven opener kinds from an empty column and from one pane on the right, both asked before either lands, never leaves more than two panes; all of them at once fill one column) and RightColumnEntryPointTests (a LinkOpener open and an AgentTabStore open each wait for the open under way, then decide). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * chore(next): refresh the defaults snapshot for the screen-frames change PaneHandlers.neighbor and PanePlacementRouting's autoLayoutTarget and route now read the frames of the pane's own screen (navigationFrames(ofScreenContaining:)); the defaults guard hashes them as tunable sources. Nothing a person gets by default changes on the active screen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): a pane with no width is unmeasured, not too narrow to split Fleet run of #28044 at 8e52036 (scratch #28392): - SideOpenStripTests failed: its pane has no window, so its width was 0, and the narrow-chat floor (5ade38f) took 0 pt for too narrow to split, opening the link in place. RightColumn.width(of:) answers nil for a pane with no width yet; LinkOpener.target and RightColumn.measure use it, so an unmeasured pane sets no floor, as before the floor. - RightColumnScreenTests required the chat's screen to be hidden after the switch, but its fade ends on later frames a headless window never draws. It requires the screen to be inactive instead; a hidden screen view keeps the same geometry, so the lookup reads it either way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * fix(next): a pane not in a window is unmeasured: its placeholder frame is no width Fleet run of #28044 at caf8e69 (scratch #28392): SideOpenStripTests still opened its link in place. A PaneContentView starts at a 400 pt placeholder frame, so a pane not yet laid out read as 400 pt, under the 480 pt chat floor. caf8e69 only treated a 0 pt width as unmeasured; RightColumn.width(of:) now answers nil for any pane that is not in a window, then for a 0 pt one. SideOpenStripTests.aPaneNotInAWindowIsUnmeasured covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-0c8df2029ad2a028c4e7 * Route chat dock terminal and browser opens down the right column Use the serialized LinkOpener placement for dock tools, before terminal tiling, while preserving working directory, browser URL and profile. Keep the New Tab page route unchanged. Validation: git diff --check passed. verify-local portable checks passed except untouched localization debt; Swift parsing unavailable because this host has no swiftc. Fleet ChatDockToolPlacementTests remains required. Agent-Session: https://cmux.com/deeplink/agent?id=aid-aed5521ea76b3c8830d5 * fix(next): place chat header opens in lower right pane Agent-Session: https://cmux.com/deeplink/agent?id=aid-7f14d5b336ee0cc28989 * docs(next): describe measured chat header placement Agent-Session: https://cmux.com/deeplink/agent?id=aid-7f14d5b336ee0cc28989 * fix(next): preserve header terminal toggle anchoring Agent-Session: https://cmux.com/deeplink/agent?id=aid-7f14d5b336ee0cc28989 * cmux NEXT: a link or terminal opened on the right goes to the pane that already holds its kind Leo, 2026-10-31: "imo in this case, just open it on the existing. you only need the rightmost botom auto split when there's a terminal that gets opened when there's already a browser, and vice versa". RightColumnPlacement.decide takes the kind being opened (terminal or browser) and which kinds each right-column pane holds: a pane already holding that kind gets the tab; only a different kind splits below. Links (openWeb, a browser page's Cmd-click) open as .browser, folders as .terminal. Chats, subagents, side sessions and file pages pass no kind and place as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-8451c317c43e92348de8 * LinkOpener: name the holds closure's arguments Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-8451c317c43e92348de8 * refactor(next): keep link placement in the pane domain Agent-Session: https://cmux.com/deeplink/agent?id=aid-5bd2469d5ee8ac333149 * Keep dock adapters together and move pure pane policy to AgentPane Agent-Session: https://cmux.com/deeplink/agent?id=aid-f81e89ac8a39b3f54b68 * Preserve outbound selection and test tool-kind routing Agent-Session: https://cmux.com/deeplink/agent?id=aid-f81e89ac8a39b3f54b68 * Keep dock geometry coverage advisory without a display Agent-Session: https://cmux.com/deeplink/agent?id=aid-f81e89ac8a39b3f54b68 * Verify header documents route from the chat instead of the last panel Agent-Session: https://cmux.com/deeplink/agent?id=aid-f81e89ac8a39b3f54b68 * Fix browser forwarding signature and serialize header pane moves Agent-Session: https://cmux.com/deeplink/agent?id=aid-f81e89ac8a39b3f54b68 * fix(next): link the pane placement release highlight Agent-Session: https://cmux.com/deeplink/agent?id=aid-5bd2469d5ee8ac333149 * fix(next): link the pane placement release highlight Agent-Session: https://cmux.com/deeplink/agent?id=aid-5bd2469d5ee8ac333149 * Avoid nesting Swift Testing macros in dock placement coverage Agent-Session: https://cmux.com/deeplink/agent?id=aid-f81e89ac8a39b3f54b68 * Keep working-context URL and folder policy in AgentPane Agent-Session: https://cmux.com/deeplink/agent?id=aid-f81e89ac8a39b3f54b68 * Apply the pane helper namespace convention Agent-Session: https://cmux.com/deeplink/agent?id=aid-f81e89ac8a39b3f54b68 * fix(next): import dock placement policy module explicitly Agent-Session: https://cmux.com/deeplink/agent?id=aid-f81e89ac8a39b3f54b68 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
Closing as superseded by #28989 (for its lane, ux-tabs-panes-r2). |
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Leo, 2026-10-30:
Earlier, 2026-10-28 (the spec, leo-workspace
design/pane-placement.md):Stacked on #27789. This PR's own diff is
ef884b554e8f..HEAD. It stays a draft until #27789 lands.The rule
One function,
RightColumnPlacement.decide(CmuxNextAgentPane), decides every open on the right of a pane. Its input is the right column the window actually shows (RightColumn.measure, from the pane geometry):Paths routed through it
Transcript subagent / Task cards (
AgentSubagentPanes): cmux NEXT: subagents fill one split below the first, then open as tabs #27789's placement function, now shared and reading the window's geometry instead of the tracker's own list.Side chats,
/btw, Session Info sessions, transcript session cards (AgentSidePanes;chat.sideChatandchat.offer.open).Links opened to the right, and chats opened from Needs you (
LinkOpener/LinkOpenPlacement): new.splitBelowtarget for a source that takes tabs.openWeb's reuse of a page already open on the right,openFolderandDurableTabLink's fallback handle it.Main-pane layout (ux-owner, 2026-10-30, on Leo's "anything we expect to open on 'the right'"): the strip column right of the docked chat is its right side, so the same lower split applies (
LinkOpenPlacement.inColumn,openOnRight). With nothing on its right yet, main-pane layout still makes the column by its own rule.Unchanged: Home's pinned dock (a different surface, not named) keeps the link rule: the pane on its right, else a column beside the dock.
Overlap
This overlaps #26582 (
RightPanePlacement, the general document/session policy, owned by another lane), which doesn't touch these paths yet. This PR doesn't take its type or touch its branch. Every path here calls one pure function, so it can fold into that policy later.Tests (one per path)
RightColumnPlacementTests: the rule itself, narrow floors, and close-collapses-then-splits-again.AgentSubagentPanesPlacementTests: with the right side occupied, the first subagent splits below it and the second is a tab there; closing collapses; a 4th subagent is a tab, never a third split.AgentSideSessionPaneTests: side sessions split right, then once below, then become tabs in the lower pane; with the right side occupied they split below it; closing the lower pane's tabs collapses it and the next open splits again.LinkOpenPlacementTests: with the right side measured, a link beside a pane splits below it, a full column takes a tab in its lower pane, narrow columns take a tab, and the main-pane strip column gets the same lower split.Spec: leo-workspace
design/pane-placement.mdgains "Something already on the right (2026-10-30)" (lane/ux-pane-placement-lower).Screenshots
Before/after: pending a fleet capture (requested from ux-owner for the media tour). They'll be added before this leaves draft.
Risk: a behaviour change in where the 2nd+ item opens on the right, for all of these paths. No new timers, observers or animation loops.
Agent session: agent session
🤖 Generated with Claude Code