cmux NEXT: picks and typed commands from the chat dock open beside the chat, not in the main pane

Closed
TE
5d ago
+431-20

Leo, 2026-10-30, of an agent chat that opened another agent's chat (and links) while a github.com page was already open in a pane on its right:

"stupid message + stupid animation to open up a tab in the main pane THEN opening up a right pane and animating that + stupid to not reuse the existing opened fing right pane + stupid to put something THIS FING CRITICAL ONTO FING [fleet host] WHICH FLAKES THE F OUT"

"IF I'M FOCUSED ON THE MAIN PANE OPEN IT THERE IF IM NOT THEN OPEN IT F***ING IN THAT OTHER PLACE"

"IT'S THE FING SINGLE MAIN TO OTHER FING"

Of new tabs (Cmd-T), which an earlier head of this PR changed:

"NOBODY FGUCKING ASKED FOR THIS ONE TOO"

Then, on what he meant:

"Bro, obviously I'm saying, like, Command T. It opens up Command T, the new page, the new tab page. But also, like, oh my fing god. The fact that, like, opening up a new browser, a new terminal, it opens up stuff, like, in the right pane. It's just, like, the fing logic for that is so fing borked. Because it keeps opening up fing columns instead of going at the bottom. I don't know if that's the intention here of the tri-column or, like, something else, but it's, like, oh my god. Claude would just open it up on the right side's bottom instead of what we have now. And I'm obviously saying not to fing open it up in the main fing pane. God f***ing darn it."

Earlier asks this keeps:

2026-10-29: "I open a link ... it opens up on the right by default. Then just open it up on the f***ing right."

04:52: "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."

After the base merge (5ad007d1)

The right-column half of this PR is now on base through #28989, the carry of #28044. Everything a chat opens (links, a chat it opens, side sessions) goes to the column on its right: a split below its one pane, else its lower pane, never a new column. That follows Leo's later ask on that PR: "like having it go split instead of just adding a third colujmn". This PR's own "a tab in the pane already on the right" path (LinkOpener.target(fromChat:), existingRight, rightPaneTab) conflicted with it and would have undone it for chats, so the merge takes base's LinkOpener.swift and AgentTabs+Header.swift and drops those three. The sections below that describe them describe the earlier heads.

What this PR still changes (10 files against base): picks on a New Tab page in the chat dock open in the strip beside the chat (the web page and the native page alike), a typed !command from the dock opens beside the chat (a script's submission opens where it asked), Cmd-T keeps the main-pane rule Leo named, and the link test fixture.

What was wrong

The chat opened the other agent's chat through chat.offer.open (the side-session path, AgentSidePanes). That path only reuses a side pane it made itself, so with the GitHub page on the right it asked for a new split. AgentTabStore.openBeside then made the tab in the chat's pane and moved it to a new split on the right. That gives all three things he saw: a tab in the main pane first, a second right pane sliding in, and the existing right pane left unused.

What changes

What a chat opens goes to the pane already on its right, made there directly. Nothing is made in the chat's pane and then moved.

  • LinkOpener.target(fromChat:) checks for that pane first. existingRight(of:) finds it: the drawn neighbour if it takes tabs, else the first pane after the chat in column order (a right dock, or a scrolled column the drawn frames miss).
  • What uses it:
    • links clicked in a chat (openWeb with reusesTab, including its "select the tab already showing this page" check);
    • a chat's open-on-right (tabs.openRight);
    • side sessions with no side pane of their own yet (openSide, then rightPaneTab, then openBeside(..., split: false), which makes a tab in that pane).
  • Only panes of the same machine count for side sessions.
  • There is no "chat's own pane" alternative.

Cmd-T in the main pane opens the New Tab page there ("IF I'M FOCUSED ON THE MAIN PANE OPEN IT THERE").

  • A person's Cmd-T in the chat dock opens the New Tab page in the dock (ChatColumnPlacement.keepsNewTabPage). It used to go to the strip.
  • The page is an agent tab, so the dock still holds only agent tabs.
  • Home's pinned dock keeps its own route.
  • From any other pane, Cmd-T opens in that pane, as before ("IF IM NOT THEN OPEN IT F***ING IN THAT OTHER PLACE").
  • A lone chat already kept its page in place.

Nothing but agent tabs opens in the main pane ("not to fing open it up in the main fing pane").

  • A terminal or browser picked on a page in the chat dock opens in the strip pane beside it (ChatColumnPlacement.pagePick, the pane used last, else the first). That holds whatever else the dock holds, a page left alone included, and whatever the newTab.picksFollowLinks toggle says. The web page (replace) and the native page (submitNative, tabs.nativeNewTabPage) both go through it.
  • One picked on a page beside a lone chat takes the page's place, then the chat docks left (MainPaneLayout.dockChat), as a link from a lone chat does.
  • A chat picked on the page still opens in the dock.

Not changed

  • A browser address or search typed on the native page beside a lone chat (not docked yet) still opens in the chat's pane. That path answers with no tab to dock beside, so the chat can't dock. In the chat dock it opens in the strip like the rest.

  • New tabs other than the New Tab page (a new terminal or browser from the chat) route as on base: to the strip.

  • With nothing on the chat's right, opens split as before. Making that new pane without the tab first showing in the chat's pane is a separate PR (agent opens first, through conversation-tab-split-v1).

  • "Right side splits downward, never a new column" (a new browser, terminal, link or subagent going to the lower half of the right column) is #28044, which this PR does not include.

  • "stupid message" is the peer-unreachable card, which belongs to the queued-messages-strip lane. The fleet-host part is about where the session ran, not app code.

  • The 2 s hold that keeps a moving tab out of the source strip (OutboundTab), and the new-tab and pane animation settings.

Overlap

#28044 (the right column's lower pane) also changes LinkOpener.target and the side-session opens. Whichever lands second takes both: #28044 decides between a tab and a split below inside the right column. This PR makes a chat's opens use that column when it exists instead of opening a new one.

Tests

ChatLinkReusesRightPaneTests:

  • thePaneOnTheRightIsFoundByColumnOrder: column order finds the right pane; nothing on the right gives nil; a found right pane is the target, not a split.
  • cmdTInTheMainPaneOpensThePageThere: the chat dock keeps its New Tab page; other new tabs from it still go to the strip.
  • cmdTInAnotherPaneOpensThere: from the strip pane, the page opens in that pane.
  • aPickOnAPageInTheChatDockOpensInTheStrip: a pick on a page in the dock goes to the strip (the pane used last first), whatever the dock's tabs. A zoomed screen, a page in the strip and Home's pinned dock are left alone.
  • theNativePagesToolPicksGoBesideTheChat: on the native page, terminal and browser picks go beside the chat; a chat pick stays.
  • homesPinnedDockAndPlainPanesKeepTheirRoute: Home's pinned dock, a lone chat and a plain dock are not the chat dock.

Screenshots

After (39fa77d8, fleet capture mini, 2x, light and dark): Cmd-T in the chat opened the New Tab page in the dock (the main pane), and the clicked pull request link (example.com/pull/4321) opened as a tab in the pane already on the right, next to "Pull requests". There is no new split, and nothing but agent tabs is in the main pane.

after light

after dark

Before (merge base a94df426, same state up to its github.com page; base has no "link" fixture): Cmd-T in the chat put the New Tab page in the right-hand strip instead of the dock.

before light

before dark

Stills are cropped to the window's top 1060 px; the terminal tab's path title is masked in all four.

Not shown in the stills: the chat opened from the transcript (chat.offer.open). The fixture's sender chat does not exist on the capture mini, so that click opens nothing in either capture. That path is covered by the code (tabs.rightPaneTab, then openBeside(..., split: false)) and by ChatLinkReusesRightPaneTests.thePaneOnTheRightIsFoundByColumnOrder, which tests how the right pane is found, not the open itself.

Viewed after image: yes
Visual verdict: Leo would accept this next to ChatGPT or Claude: Cmd-T opens the New Tab page in the chat dock, the link reuses the existing right pane as a tab with no new column, and the main pane holds only agent tabs.

Agent session: agent session

šŸ¤– Generated with Claude Code