fix(focus): keys typed into a page right after an address-bar load reach it (dogfood port of #32537)

Draft
LA
4d ago
+743-187

Summary

Keys typed into a web page right after it loads from the address bar were lost (P0 browser-click-then-type; journey kit cp/reopen-video-kit@4ff1d5e5ce6). Typing "fieldkeysok" into a focused text field left "fok", and the page logged a click no one sent. Two causes, both fixed here:

  1. Link hints took the letters (the root cause, STATUS 7518 / 7543). After an address-bar load, Chromium reports a letter typed into the focused field as one no editable field took: CEF's focus_on_editable_field is stale (CEFShim/src/shim_client.mm:660; filed as #32573, owner PL-cef-tab). KeyRouter.routePageKey then runs the single-key page binding: f starts link hints. The session took the next letters as hint keys, picked the only target (the field) and clicked it; then o, k reached the field.
    Fix, for the two cases of f (lead review):
    • A text field has the keyboard (case 1): the f reaches the field first (Chromium reports a letter as unhandled only after the page saw it). The link-hint session's collect script (sessionCollectScript) returns the page's own answer to whether a text field has the keyboard (the focused element through shadow roots: text inputs, textarea, select, contenteditable, a focused frame, designMode). It does: the session ends with no labels (.passThrough) and gives the keys it held while the page answered to the field, once each, in the order typed (LinkHintHeldKeys). The field shows the whole word.
    • No text field has the keyboard (case 2, link hints start): unchanged from before this PR. What "before" is, checked in the code: the dispatcher passes a plain f to the page first (KeyRouter.isPageKey, so the pre-key hook does not take it), and cmux starts hints only from CEF's OnKeyEvent (CEFShim/src/shim_client.mm:644), which runs for a key the renderer did NOT consume. So a page shortcut on f that handles its key (YouTube full screen, Gmail and GitHub shortcuts call preventDefault) runs alone, and hints do not start; there is no double action. Only a page that acts on f without consuming it gets both, as with Chrome hint extensions (Vimium). This is the design from before this PR, not a defect, and this PR does not change it. Letters typed before the labels show are hint keys: held and checked when the labels show, never sent to the page.
    • Every key, once, in order (lead review: no reorder, no loss, no doubles). A key's place is decided by the key event's own timestamp, not by when it is dispatched (LinkHintHeldKeys.route(typedAt:)). A key typed before the session started is no hint key and is never held: it goes on to the page on its normal path, once. That covers a key still queued when Chromium's report of the f started the session (it reaches the page now, before the keys typed after it) and a key Chromium sends back through NSApp.sendEvent after the page left it unhandled (the same event; it stays in that redispatch, as with no session). A key typed after the start is held, and on .passThrough is sent to the page once, marked as a replay (LinkHintReplays) so no session takes it again. A synthetic key with no time (timestamp 0) is the session's. No key is held twice, and no key is lost.
      Window hints keep the plain collectScript.
  2. The page window did not get the keyboard. cmux gives a Chromium page focus through CEF SetFocus(true) and relied on it to make the page's child window key. After an address-bar load it did not: the cmux window kept the keys with itself as first responder. Now CEFTab.grantFocus, the grant every path uses (direct, at browser attach, when the page shows), hands the keys from the cmux window that shows the page to the page window when CEF moved none. Still one key change per request (#24913); never from another window or an inactive app; a hidden page is never granted.

Why this layer: Chrome has one key window per browser window that forwards keys to the focused renderer. With in-process CEF the page is its own child window that must itself be key, and SetFocus is cmux's one call to make it so; cmux now checks that call's result where it makes it. With the Chromium Host (pages as views) there is no page window and nothing changes.

Cost on the typing path: none per key. The hand-over runs once per focus grant (one scan of the cmux window's child windows). The page script runs only when f/F starts link hints, never per key.

Testing

Dogfood port of #32537 (same commits, adapted to this line; blockingDecode for JSON as this line requires). Fleet, 5 runs each, proof-step.py (step ids follow in a comment when done):

  • Hand-over red: 7ea7df2ea62, CEFTabHostFocusTests (3 in-process hand-over tests).
  • Link hints red (case 1 and case 2): 28cc0d2c21a, LinkHintTypingTests.
  • Endless loop red: 55408510e21; doubled letters red: e61d7ec31de; lost queued key red: 80a20642471, LinkHintTypingTests.
  • Green 13b9b50b850 (this head): LinkHintTypingTests and CEFTabHostFocusTests 5x, ColdBrowserFocusKeysTests, PageFirstKeysTests, BrowserFocusModeKeysTests.
  • Journeys (REOPEN VIDEO, real keys on a mini, kit 4ff1d5e5ce6): browser-click-then-type and reopen-browser-plain-keys, 3 rounds each on build c1cac86f1bbb012e139c9604 (this head).

Observed / Not run

  • Observed so far: the diagnostic round (STATUS 7518): the app's own key path (no CuaDriver) and CuaDriver lose the same keys on a fresh page; a restored page gets all of them; the page window is key and Chromium's view is its first responder in all cases. Base journey red on dogfood 44dc8431.
  • History of this PR: v5 looped without end; v6 doubled the first letters; v7 could drop a key typed before the session but dispatched after it. The first version (ChildPageFocus hand-over) covered only the direct grant (review FIX); 637d3fc did not compile (missing import AppKit); 1972dd8 did not compile (WindowHintController switch); a version that took no key before the labels showed sent hint letters to the page (lead review: a link-hints regression; reverted to holding them).
  • Not run: a click into the page (CuaDriver cannot deliver a verified click into a page window on the minis without a screen-recording grant). The journey covers typing after focus only.
  • Cycle B of reopen-browser-plain-keys (keys typed before the restored page exists) may be a separate defect; it is reported per round.

Changelog

Fixed: Keys you type into a web page right after it loads from the address bar reach the page, also when the word starts with "f"

Proof

No capture: key routing; the proof is the tests and the journeys.

Base findings (not caused by this PR, STATUS 7582)

  • CEFTabHostFocusTests.aPageWindowTargetNeverTakesFirstResponder fails 5/5 at the dogfood base.
  • ChildPageFocusKeyChangeTests.aReapplyInsideTheGrantDoesNotGrantAgain fails on this line: ChildPageFocus.reclaim lacks the granting guard feat has.