Right-side opens: make subagent and side-chat tabs in their split, skip the history fetch for a new link tab
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.BrowserTabServicemarked a surface "opened here" only after the reply, so at pane-added timewasOpenedHere(tab)was false. ChromiumNewTabNowtherefore refused the tab, which then took the restored-page path. That path (BrowserHistoryStarts.chromium) does afetchHistorydaemon 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,
OpeningSplitsrecords its split pane.wasOpenedHerealso answers true for a tab listed in that pane. The entry ends in adeferonce the reply arrives. Incognito keeps its current path.
Subagent / side chat / fork (web showSubagent → AgentTabStore.openSubagent → AgentSubagentPanes.open → openOnRight (RightColumnTurns) → place → openBeside):
openBesidemade 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, butopenBesidenever used it.- Fix: when the daemon serves the capability and the direction is right or down,
openBesidenow makes the tab in its split with onenew-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:aForkToTheRightIsMadeInItsSplitNeverInTheChatandaSubagentOnTheRightIsMadeInItsSplit: red at 1ed1a092 (no.splitPaneintent,split == nil), green at head.withoutTheCapabilityTheTabIsMadeInTheChatsPaneandonlyRightAndDownAreMadeInTheirSplitpass at head.
LinkOpenSplitDirectTests.theSplitsTabIsOpenedHereBeforeTheReply: red at f7379b8f (openedHere → false), green at head.- Also green at head: the rest of
LinkOpenSplitDirectTests(13),RightColumnEntryPointTests(3),MoveTabToNewSplitFocusTestsandIncognitoSurfacesTests. - 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.
| metric | before | after | delta | harness |
|---|---|---|---|---|
| link_right page_ms, under load, median (n=10) | ~1390 | ~715 | about -49% | instant-new-surfaces, steps 08f882c2 → ec7a6185 |
| link_right page_ms, under load, max | 1960 | 999 | -961 | same |
| link_right opens that made a new CEF window under load | 7/10 | 2/10 | -5 | same |
| link_right page_ms, warm idle, p50 / p95 | 211 / 295 | 178 / 259 | -33 / -36 | same |
| link_right confirmed_ms, warm idle, p50 | 224 | 195 | -29 | same |
| link_right confirmed_ms, warm idle, p95 / max (old probe) | 2872 / 5030 | 301 / 323 | -2571 / -4707 | steps db86b795 → 365ab7b3 |
| link_right page_ms, cold, p50 (n=4, too few to call) | 393 | 266 | -127 | steps 08f882c2 → ec7a6185 |
| subagent click → first content, idle, p50 (n=5) | 674 | 729 | +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 whenpending.value()returns or throws, followed byTabMirror.reaches, which is bounded at 10 s. On failure it callsfinished(false), so the turn always ends.- An
OpeningSplitsentry is removed by adeferonce thenew-frontend-browser-tabreply 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:showSplitonly sets a focus intent for.useropens. - 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