Fix desync after Cmd-N: a late Chromium page gets the keys through the focus model; G1 counts cmux pages
Summary
The action fuzzer (#29071) recorded three debug.desync findings after Cmd-N (STATUS 1002, 1022). This PR classifies each one and fixes it at its cause.
W1 and W4: a real desync. At the first Cmd-N of a launch, no New Tab spare exists and Chromium is not started, so the New Tab page (an agentPage) shows before its Chromium page exists. The focus applier makes the page's WebKit stand-in first responder. When the Chromium page attaches, PageWebView.attachChromium granted Chromium focus by itself, behind the focus model. The stand-in stayed first responder, and the applier never recorded the focused page. When the app is active, the page window's key change (childWindowDidBecomeKey) makes the applier apply the model again. When the app is not active (the fuzzer's capture slot, or a person who switched away before the page attached), nothing applies it again. The world check then keeps W1 appkit content:pane_* (PageWebView) != model agentPage and W4 chromium page nil != model tab_*. The window becoming key again heals it.
Fix: when a page's Chromium page attaches, the page asks its Chromium host (PageChromiumHosting.focusViewChanged, default: no focus model). The app's host finds the pane whose own Chromium page (TabContent.childWindowTab) this is and sends contentPresented. The applier then applies the model as it does for any newly presented content: no responder in the cmux window, the page recorded and granted, in one key change. Any other page (the docked Home's Channels page, a page that no pane shows, a parked spare) keeps its own grant, as on the base. A fall back to WebKit is unchanged.
G1: a false finding of the check. ChildPageGeometry.sample counted only browser tabs as hosts. The page window of every shown cmux page in Chromium (the New Tab page, an agent chat) was then reported as page window (618, 268, 892, 692) covers no pane. Hosts now come from TabContent.childWindowTab, the rule that the focus applier and the input observation already use. The same sample drives WindowOverlayLayer.pageWindowDidChangeFrame, which forced a page update on each frame change of such a page window. ChildPageGeometry.check had no caller and is removed.
Testing
Red first: PageWebViewFocusViewChangeTests (CmuxNextPagesTests), added at a346e23e41b.
- Red at a346e23e41b: fleet step 7639b30f74c1aeb8dd556828 failed with 5 issues on its own assertions (the host was not told; the page granted focus itself:
page.focusAsks → [true]). - Green at 976b215bed7: step d15a4188247c739fcd544de5; at 5ceaf206d2b: step 095abe66ff1b6e4e1e31afd7. Both passed PageWebViewFocusViewChangeTests, PageWebViewChromiumTests and ChromiumPageFocusInvariantTests. The head ad21c67e758 only removes a no-op call and one test case of it, and corrects docs.
- Compile of the head ad21c67e758: job 155f35f2347c86058d4061c8 (CmuxNext with tests, app, cmux-tui) passed.
- Journey
found-desync-cmd-n-page(#29274): one Cmd-N, a 3 s settle, thendebug.desync checkmust list no G1, W1 or W4.
Observed / Not run
Observed (journey, cmux19, exclusive capture lease, diagnostic mode):
- v2.2.11-opt (78ca62c225bb0943594e3cd2), app inactive: RED, W1 + W4 + G1 13 s after Cmd-N;
debug.focus: first responder PageWebView, model agentPage. - feat-tip-fixes-v2 (f2f3083bac80ab71101d03ac, feat acfd69382dc), app active: RED, G1 only (twice).
- This PR at 976b215bed7 (build 568ad9fbd033e4ba0ac995fb), app active: PASS (no G1, W1 or W4).
- Correction (coordinator, 11:4xZ): build 2db9e5949fd59e393aa1bab7 (5ceaf206d2b) did NOT run this journey. It ran a later dropped "-inactive" variant, which FAILED on its own
app_active == falseprecondition. Do not count it. - The reviewed journey (#29274), whole host with the app active, through the mini scheduler (spec desync-g1w1w4-journeys), 12:11-12:19Z:
- feat base f2f3083bac80ab71101d03ac, cmux20: RED, G1 only (no window unsettled; key window CefNSWindow).
- this PR's head ad21c67e758 (build ed24b7b111a5fe9b44eaf569), cmux20: PASS (no G1, W1 or W4, no window unsettled).
- Home dock Channels, first open (the review's FIX case at 976b215), journey found-desync-channels-type: the SAME state on the feat base f2f3083b (cmux19), on 976b215 (build 568ad9fbd033e4ba0ac995fb, cmux19) and on this head (build ed24b7b1, cmux20): Channels in Chromium never gets the keyboard; the key window is ShellWindow and the first responder is the hidden PageWKWebView. This is a pre-existing bug on feat, not caused by this PR, and this PR does not fix it. The journey cannot tell 976b215 from the head; the identity match is shown by the code reading and the review only.
Not run, or read only:
- W1/W4 on a feat build with the app inactive. In the capture slot, feat builds become active after Cmd-N, so the journey cannot make that state there. The inactive path on feat is shown by the Swift test and by the code reading above, not by a journey.
- No app-level test of the pane lookup closure in AppDelegate (the arch ratchet refused the App lines for a testable extension). The review accepted this.
Changelog
none
Proof
No capture: there is no visible change. The fix changes which view holds the keyboard after a cold New Tab page attaches while cmux is in the background, and what a debug-only check reports.