perf(browser): closing a Chromium tab, or a pane's last one, takes effect on the click (beforeunload still asks)
Summary
Leo (2026-11-03): "it's so slow to open and close idk if there's some sort of threading issue but it's nigh instant for claude and codex so we nede to be at least as fast". Leo (2026-10-25): "closing the right pane seems be like... waiting for the right tab's closing animation to play before closing".
leo-named-fix
A person's close of a live Chromium tab (Cmd-W, its close button, close others/left/right) asked the page first (beforeunload, BrowserUserClosing, #22745) and left the tab in its strip, and for a pane's last tab the whole pane on screen, until Chromium's renderer answered and the page's own close removed it (CloseUndoToasts.close kept every deciding tab; TabCloseGuard never hid one). Chrome takes the tab away at once and only brings it back when the page actually asks.
Now:
- The deciding tabs leave their strip in the click's turn, and a pane whose last tab that was leaves its window in that turn too (
PaneController.showPaneClose). The page stays alive while Chromium decides: the hide is a close intent nothing sends (HiddenTabClose, the same mechanism #31875 uses for terminals), and the store keeps listing the tab so its view and page stay (holdTabs). - The page's own close (no handler, or Leave):
closeTabon the tab lifts the hide in the same turn its close takes the tab out again (no flash); a multi-tab close still joins it into one Reopen Closed step (PageDecidedCloses). - Only a page that asks (its beforeunload "Leave site?") brings its tab back in place, selected, with its pane (whichever close hid it), before the question. Stay keeps it; Leave closes it. A page that has neither closed nor asked within 5 s is closed without asking (the close took effect on the click), unless its question arrives right then, in which case the tab comes back for it.
- The page's own close request (
BrowserPageRequests.close) goes through the held pane, so an emptied pane's pages still close as one Reopen Closed step, and a closed tab stays hidden until its grouped close prepares it. - New
HiddenPageCloses<Pane>(CmuxNextDaemonService) holds the hides; CmuxNextApp only wires it (CloseUndoToasts.hidePages, one lift line inTabLifecycle.close, one restore line inBrowserPageRequests). To keep CmuxNextApp's line count from rising,ReopenTimingmoved from CloseUndoToasts to CmuxNextDaemonService (no behaviour change).
Not touched: terminal closes in TabCloseGuard and the rows-column pane close (other lanes).
Risk: close-path state. A page whose renderer neither closes nor asks within 5 s is closed without asking. A tab hidden while its page decides is out of the visible records, so an action that names it by id in that window (other than Close Tab, which lifts the hide) finds no tab.
Testing
Regression tests (committed red first, c9cc44c1d44), BrowserTabCloseLeavesAtOnceTests against the same-tree daemon:
aDecidingPagesTabLeavesTheStripInTheClicksTurn: a deciding page's tab is out of the strip right after Close Tab; the page's close then closes it.aPaneWhoseLastTabIsADecidingPageLeavesInTheClicksTurn: the right pane's only tab decides; the pane is closing in the click's turn; the page's close closes the pane.aPageThatAsksBringsItsTabBackSelected: the tab comes back at its place, selected.aPageThatNeverDecidesClosesItsTabAtTheDeadlineAndNeverBringsItBack: the 5 s path closes and never restores.- Review regressions, each committed red first:
thePagesOwnCloseRequestClosesItsHiddenTabAtOnce(red be1b7d6439e: closed only at 5.28 s),closingAnEmptiedPanesDecidingPagesIsOneReopenStep(red 18047015b2d: 2 Reopen Closed steps),aPageThatAsksAfterALaterCloseEmptiedItsPaneBringsThePaneBack(red c4d9d4f3852),aQuestionAtTheDeadlineBringsTheTabBackNeverClosesIt(red cb481c8c531: the asking page was closed),aPageThatAsksAfterItsPanesCloseLandedBringsThePaneBack. CloseOtherTabsReopenTests.pagesThatDecideTheirCloseJoinTheSameReopenStepupdated: the deciding tabs now leave the strip at once, and the press is still one Reopen Closed step.
Command: cmux-ci run --class light --script scripts/ci/package-test-lane.sh --ref SHA --arg=suite --arg=Packages/macOS/CmuxNext --arg=BrowserTabCloseLeavesAtOnceTests,CloseOtherTabsReopenTests,CmdWOnReplacedStandInTests,PageDecidedClosesTests,CloseTabKeepsPaneTests,RapidCloseTabTests --env CMUX_CI_XCODE_APP=/Applications/Xcode_26.6.app
- Red at c9cc44c1d44 (base + first tests): "the closed tab waits in its strip for its page", "the pane waits on screen for its page to decide".
- Green at head 7947720edaa: all 6 suites passed (step 8fb1bd43b9376eabf609b688): BrowserTabCloseLeavesAtOnceTests (11 tests), CloseOtherTabsReopenTests, CmdWOnReplacedStandInTests, CloseTabKeepsPaneTests, RapidCloseTabTests, PageDecidedClosesTests. CmuxNext compile check passed at the head.
scripts/cmux-next/prepush-gate.py: no new violations; CmuxNextApp lines do not rise.
Performance
Same harness at both ends: BrowserTabCloseLeavesAtOnceTests/measureCloseToStripDrop. 20 rounds of a person's close of a browser tab whose page decides (injected pageDecidesClose; the page answers no sooner than 1 s, standing in for a renderer round trip), timed from the close action to the tab leaving its strip. The base side runs the same test file at base (branch lane/right-pane-close-fast-browser-measure-base, 6579ba0b3b9).
| metric | before (base 2023a5d9fdb) | after (head 7947720edaa) | delta | harness or run link |
|---|---|---|---|---|
| close -> tab gone from strip, p50 | did not leave within 1 s, 20/20 rounds (waits for the page) | 0.62 ms | at least -999 ms | measureCloseToStripDrop, cmux-ci steps cd94a05/rpcfb-mbase2 (base) and 8fb1bd43b9376eabf609b688 (head) |
| close -> tab gone from strip, p95 | did not leave within 1 s | 4.01 ms (0.61-1.31 ms on other runs) | at least -996 ms | same |
| tabs that left in the click's turn | 0 / 20 | 20 / 20 | +20 | same |
On base the tab waits for Chromium's answer (a renderer round trip, longer under load). This harness caps that wait at 1 s, so the base number is a lower bound. The newtab-flash chat-browser tour (close-browser-1/2/3) could not be found to rerun.
Observed / Not run
Observed: the fleet red and green runs above, the compile check, and the measure at base and head. Not run: a live Chromium page with a real beforeunload handler in a DEV build (the tests inject pageDecidesClose and drive the page's .close request through BrowserPageRequests), and the newtab-flash tour. Reviews: an Opus subagent review BLOCKed 9b3becab797, and router-review BLOCKed b24bc3a92fe, 66aaa5601f8, 7e046b696d8 and 9b8ed262253. Each blocking finding is fixed above with a red/green test. router-review gave SHIP at 7947720edaaf18e7e6439c2c2dec2dd43347363f (gpt-6-astra high).
Changelog
Fixed: closing a browser tab, or a pane's last browser tab, takes effect on the click; a page that asks "Leave site?" brings its tab back first.
No capture: the change is timing (the tab leaves a renderer round trip sooner); the evidence is the tests and the bench.
Agent session: agent session
š¤ Generated with Claude Code