Right-side opens: make subagent and side-chat tabs in their split, skip the history fetch for a new link tab

Merged
TE
2d ago
+269-20

Leo, 2026-11-05: "it takes EGREGIOUSLY long for me to open up a link/the pane on the right opens... there's probably some stupid flow that bottlenecks itself", then: "things just don't f***ing open up fast enough on the right. / links. subagents. all".

Part of the main-thread hang push (manaflow-ai/cmuxterm-hq#6338, comment 6597127823), where Leo also said: "i frequently see this / but claude: / and codex: / have much more useful right click things and dont get the application not responding thing".

What was slow

Every open on the right (links, files, subagents, side chats, forks) goes through RightColumnTurns, which runs one open at a time per source pane. A turn ends only when that open's done fires, so whatever is slow inside one open also delays the next one. I found two waits inside the turn.

Link (click → PageWebView.chromiumMessage → AgentPaneNavigation.openOutside → LinkRouter.open → LinkOpener.openWeb → turns.serially → .splitRight → PaneBrowserTabOpener.openInSplit → BrowserTabSplitCommand.intendNow (split shown with a stand-in tab) → new-frontend-browser-tab {split} → pane-added → TabContentCache.browser(for:) → CEFEngine.makeReadyTab → first paint):

  • The daemon's pane-added usually arrives before the reply to new-frontend-browser-tab. BrowserTabService marked a surface "opened here" only after the reply, so at pane-added time wasOpenedHere(tab) was false.
  • ChromiumNewTabNow therefore refused the tab, which then took the restored-page path. That path (BrowserHistoryStarts.chromium) does a fetchHistory daemon round trip with a 1 s budget before it creates the CEF tab.
  • So a fresh link paid a history fetch it never needed before its page started.
  • Fix: while a split-first open is under way, OpeningSplits records its split pane. wasOpenedHere also answers true for a tab listed in that pane. The entry ends in a defer once the reply arrives. Incognito keeps its current path.

Subagent / side chat / fork (web showSubagent → AgentTabStore.openSubagent → AgentSubagentPanes.open → openOnRight (RightColumnTurns) → place → openBeside):

  • openBeside made the tab in the chat's own pane (one daemon round trip) and waited for the store to list it.
  • It then ran move-tab-to-split (a second round trip) and waited again. The split showed only after both, and meanwhile the new tab flashed in the chat's tab strip.
  • conversation-tab-split-v1 (which makes the tab directly in a new split) already existed, but openBeside never used it.
  • Fix: when the daemon serves the capability and the direction is right or down, openBeside now makes the tab in its split with one new-conversation-tab {split}. The split shows at once with a stand-in tab, the same way link opens already do (#32900). Older daemons keep the make-and-move path.

Tests (red at base, green at head)

  • AgentOpenBesideSplitFirstTests:
    • aForkToTheRightIsMadeInItsSplitNeverInTheChat and aSubagentOnTheRightIsMadeInItsSplit: red at 1ed1a092 (no .splitPane intent, split == nil), green at head.
    • withoutTheCapabilityTheTabIsMadeInTheChatsPane and onlyRightAndDownAreMadeInTheirSplit pass at head.
  • LinkOpenSplitDirectTests.theSplitsTabIsOpenedHereBeforeTheReply: red at f7379b8f (openedHere → false), green at head.
  • Also green at head: the rest of LinkOpenSplitDirectTests (13), RightColumnEntryPointTests (3), MoveTabToNewSplitFocusTests and IncognitoSurfacesTests.
  • Compile check (app) passed at head f3f1ffff.

Performance

Measured with the fixed benches (#36070, #36145). Each pair ran on one display Mac. Before is base plus the bench fixes; after is this PR plus the bench fixes.

metricbeforeafterdeltaharness
link_right page_ms, under load, median (n=10)~1390~715about -49%instant-new-surfaces, steps 08f882c2 → ec7a6185
link_right page_ms, under load, max1960999-961same
link_right opens that made a new CEF window under load7/102/10-5same
link_right page_ms, warm idle, p50 / p95211 / 295178 / 259-33 / -36same
link_right confirmed_ms, warm idle, p50224195-29same
link_right confirmed_ms, warm idle, p95 / max (old probe)2872 / 5030301 / 323-2571 / -4707steps db86b795 → 365ab7b3
link_right page_ms, cold, p50 (n=4, too few to call)393266-127steps 08f882c2 → ec7a6185
subagent click → first content, idle, p50 (n=5)674729+55 (noise)chat-open, steps c13f1372 → e5ea1803

How to read these numbers:

  • Opening a subagent takes about the same time. The time goes to the cold agent page, not the round trips this PR removes.
  • Under load, 3 of 5 after-opens were attributed to the split's stand-in view by the bench, so that column is not comparable. Follow-ups are in #36060.

New waits and their stop conditions

  • openInSplit's task ends when pending.value() returns or throws, followed by TabMirror.reaches, which is bounded at 10 s. On failure it calls finished(false), so the turn always ends.
  • An OpeningSplits entry is removed by a defer once the new-frontend-browser-tab reply returns or throws.
  • No new timers or observers.

Risk

risk:normal. This changes where agent tabs opened on a chat's right are created.

  • An unfocused (.script) subagent open shows its split without taking focus: showSplit only sets a focus intent for .user opens.
  • The split intent uses the 0.5 ratio. A split below in a column with several panes may size a little differently from the even sizing the move path used.
  • Both are follow-ups for a check in a build.

The bench script also learns page_ms (link_right / split_browser wait for first paint) and a --link-url option.

An arch-ratchet exception for app_lines (+34) is in plans/cmux-next/arch-exceptions/right-opens-split-first.json.

Changelog

  • Links, subagents, side chats and forks opened on the right of a chat show their pane at once and start loading sooner.

Agent session: agent session

🤖 Generated with Claude Code