Repository navigation
Open the live view's soft keyboard from the remote browser's focus - #464
Conversation
|
bugbot run |
Reviewer guideThis PR is stacked on #448. To merge: merge #448, retarget this PR to What it doesWhen a live view is opened on a touch device, the VM reports which editable element has focus and where the fields are. The live view uses that to raise the soft keyboard only for real fields, set up for each field, and to close it when the field loses focus. The cursor-shape guess from #448 remains for taps on browser UI (the address bar) and for when the stream is unavailable. Where to look
Release-relevant properties, and how each is checked
Tested
Emulation-only or not verified
|
…m cannot judge unowned
… row from whether it is on screen
…ard on a touch focus
This reverts commit b726842.
2174597 to
ce0bdb0
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ce0bdb0. Configure here.
tnsardesai
left a comment
There was a problem hiding this comment.
LGTM
Future follow-ups (not for this PR)
-
Non-blocking design issue — Untagged login fields now get auto-capitals and autocorrect —
images/chromium-headful/client/src/utils/field-keyboard.ts:L65-77,L117-132- Problem:
fieldKindonly spots username and email fields fromautocomplete="username",type="email",inputmode="email"orautocomplete="email". Many sign-in forms use<input type="text" name="email|login|username|user">with none of these. Those fields count as plain text and getautocapitalize="sentences",autocorrect="on"andkeepText. Before this PR the overlay always usedautocorrect="off",autocapitalize="off"andspellcheck="false". - Impact: on iOS the first character of a username or email gets capitalized, and autocorrect can rewrite unusual usernames. That can break or slow down signing in through a touch live view.
- Fix: in
watcher.jsdescribe, add aloginflag for text inputs that look like login fields. Match name, id, placeholder or label text against something like/user|login|e-?mail|account|sign.?in/, or flag inputs that share a form with aninput[type=password]. Then havefieldKindmaplogintousername, or toemailwhen the hint says email, before it falls back totext.
- Problem:
-
Non-blocking question — Does the next letter get capitalized after every tap in a plain-text field? —
images/chromium-headful/client/src/components/video.vue:L1206,L353- Problem: in
keepTextmode the overlay resets to the bare' 'sentinel on every tap, arrow or other non-text key, and shortcut, and plain text now getsautocapitalize="sentences". If iOS treats a field holding only whitespace as the start of a sentence, the next letter gets capitalized even when the remote caret is mid-sentence. Tapping into existing text to insert a word would produce, for example, "The". - Impact: words inserted into existing text would come out capitalized and need fixing by hand. This hasn't been checked on a physical device.
- Fix: check it on an iPhone. If it happens, use a sentinel that reads as mid-sentence, such as
'a ', forkeepText.TypedTextalready strips the sentinel by prefix and treats it as one character. Another option is to handle this together with the login-field follow-up above.
- Problem: in
fwiw I haven't tried testing e2e on an ios device |

Stacked on #448 (base
hypeship/live-view-touch-input).Summary
On touch devices, the live view decides whether to raise the soft keyboard from the remote cursor shape, which Chromium also shows over selectable text. That guess cannot tell a text field from a paragraph, picks the wrong keyboard, and misses closes after a submit that navigates to a page with a search box in the same spot. This PR has the browser VM report which editable element has focus, and where the fields on screen are. The live view uses that report to raise the keyboard only for real fields, give it the field's keyboard and AutoFill, and close it when the field loses focus.
Design
In the VM:
server/lib/livefocus. It runs only while a live view is subscribed, and holds its own DevTools connection. Thebrowsersurfacetracker is used withWithoutLocations, so it enables no Page domain. Each frame of every page and out-of-process iframe getswatcher.jsin an isolated world (Page.createIsolatedWorld). The server long-polls that watcher withRuntime.callFunctionOn(awaitPromise).inputmode,autocomplete,enterkeyhint, the page's ownautocapitalize/autocorrect/spellcheck, a one-time-code flag, and its rect. It also reports whether its document has focus.elementFromPoint), including fields in same-origin iframes. The server converts these rects to screen pixels using the window bounds (Browser.getWindowForTarget) and page zoom.autocomplete="one-time-code", by one-character boxes, or by name, label or placeholder hints such as code, OTP or verification on short or numeric inputs. Zip, postal and promo codes are excluded.focusinandfocusout, on window focus and blur, on visibility changes, scrolls, resizes and DOM mutations (throttled), and on a 1s tick. Only changes are sent.Stealth. Nothing runs in the page's main world, and no Runtime, Page or DOM domain is enabled (the real-Chromium test asserts this through a recording proxy). The page cannot see the isolated world's globals, listeners or observers, and page changes to globals or prototypes do not reach it. The watcher never logs, and catches its own errors. If a watcher goes 30s without a poll, it removes its listeners and observer, so a dropped connection leaves nothing running in the page. A CDP client of the customer's own that has Runtime enabled will see one extra isolated-world context per document while a touch live view is open. Playwright and Puppeteer ignore isolated worlds they did not create.
Dismissing the keyboard:
POST /live-view/focus/blur?seq=N. Hide, the keyboard's own Done, and an iOS dismissal that leaves the overlay focused (seen as the keyboard leaving the visual viewport) all blur the remote's focused field too, the way Safari's Done does. The blur runs from the focused frame's isolated world, so the page and the live view agree and the next tap is a fresh focus. Each watcher counts focus changes and mouse presses and reports the count as the focused field'sseq. The live view sends back the count it last saw, and a blur sent before a later tap is dropped, even a tap on the field that already had focus. Closing the keyboard for the page's own reasons (a blur, a navigation, the clipboard panel), refocusing, or leaving for another app does not blur the remote field.Delivery:
GET /live-view/focus, server-sent events from the kernel-images API. This is outside the OpenAPI spec, like the PTY attach endpoint. The live view page is served at/browser/live?jwt=…, and the browser proxy already serves the kernel-images API at/browser/kernel/*with the same session token, streaming included. So the client derives the URL from its own location, with no neko change. The other route was neko's data channel. neko's legacy protocol, which this client uses, drops custom broadcasts, so that route needs a neko fork change and release, plus a privileged neko session for the API to send through. The stream sends the current state, then each change, then a ping every 15s. The client opens it only on touch devices, while hosting with input enabled and the page visible. Without the proxy route (an older image, or a localrun-docker.shlive view) or with?focusSignal=off, the stream is unavailable and the client keeps the cursor-shape behavior.Client.
remote-focus.tsholds the stream state, hit-tests taps against the field rects (in stream pixels), and ignores rects from before a client scroll until new ones arrive.focus-keyboard.tsdecides when the keyboard opens and closes, under iOS's rule thatfocus()must run inside the touch:inputmode="none", as Make live view usable on touch devices #448 does. A focus report within the wait then raises the keyboard. This covers labels and buttons that focus a field.TapKeyboard) stays for two cases. One is taps outside the page area, which is browser UI such as the address bar, where no page document has focus. The other is a live view without the stream, or with it down or stale; a keyboard raised then is not tied to a field until a focus report claims it. A keyboard raised for the address bar closes when the page gets keyboard focus back (pageFocused), which happens when the address bar navigates. The Enter-then-cursor check runs only without the stream.field-keyboard.tscopies the field onto the overlay:inputmode(email, tel, url, numeric, decimal, search).type="email"ortype="number"would sanitize the overlay's sentinel and break its selection APIs.autocomplete="one-time-code", for iOS SMS code AutoFill. Email and tel fields get the owner's own details, and name and address fields get contact AutoFill. Credentials never get AutoFill.autocorrect="off", for example. Password, username, code, email, url, tel and number fields get all three off.soft-keyboard.tstells whether a soft keyboard is actually up from how much of the layout viewport (innerHeight) the visual viewport leaves visible. The keyboard covers the layout viewport without resizing it. Safari's toolbars resize both, so they are not mistaken for a keyboard, and no learned baseline is needed, through a rotation included.keyboardShownis the one source for the extra keys row, the keyboard control's Hide label and what that control does. On a phone, or once a keyboard has been seen, it is a focused overlay with the keyboard measured up, however the focus came about. Otherwise, as with a tablet's hardware keyboard or inside a frame, it goes by the focus.typed-text.ts: for autocorrect fields, printable keys and Backspace go into the overlay instead of being sent by the Guacamole keyboard, and each change to the overlay's text is diffed against what was sent. iOS autocorrect (insertReplacementText), predictive text, dictation and composing keyboards then reach the remote as backspaces and replacement text, whatever order the events come in. The overlay starts over on a tap, on Enter or another non-text key, and on a paste or shortcut. Other fields keep Make live view usable on touch devices #448's per-key path.Desktop is unchanged: the stream is never opened, and the overlay's attributes and typing path are as before.
Testing
go test -race ./lib/livefocus ./cmd/api/.... Unit tests cover:pageFocused), subscriptions and linger, and the SSE stream;KERNEL_LIVEFOCUS_CHROME_E2E=1 go test ./lib/livefocusruns against headless Chromium with--site-per-process, 5 runs under-race. It covers:createIsolatedWorldcalls while it is shown;Runtime.enable,Page.enable,DOM.enable,addScriptToEvaluateOnNewDocumentandaddBindingnever sent.bun test tests(229 tests). The simulated iOS overlay also covers the first keyboard after loading the live view (including Safari's toolbars appearing after the load, and a keyboard that comes up after the focus), no clipboard read on a touch focus, a keyboard iOS shows for a held focus, rotation with the keyboard up or going, and a hardware keyboard. A simulated iOS overlay covers tap, Hide, and a re-tap of the same field (with and without the remote blur landing), a dismissal that keeps the overlay focused, the keyboard's own Done versus leaving the page, a focus iOS shows no keyboard for, and the address bar with Hide and a re-tap. New tests cover the focus keyboard state machine, field-to-keyboard mapping, stream state and hit-testing, the autocorrect diff, and the video component's focus, refocus and tap routing.insertReplacementTextedit replaced the word on the remote;Not verified: a physical iPhone or Android device (the real keyboard, AutoFill and autocorrect behavior, and the inputmode switch on a held focus). The CI workflows only run for PRs against
main, so they did not run on this stacked PR; the tests above were run locally.Known gaps
Note
Medium Risk
Adds always-on-while-subscribed CDP instrumentation and new HTTP routes that blur remote fields; behavior is complex on iOS but gated to touch/proxy sessions with seq-checked blurs and cursor fallback.
Overview
Touch live views no longer rely only on the remote cursor shape to decide when to show the soft keyboard. The VM exposes SSE focus state (
GET /live-view/focus) and a remote blur action (POST /live-view/focus/blur?seq=…) via newlivefocusCDP watchers in isolated worlds, reporting focused editables, on-screen field rects, and page vs browser UI focus.The chromium-headful client subscribes on touch devices (browser proxy + JWT), routes taps through focus-keyboard and remote-focus hit-testing, maps fields to overlay attributes (field-keyboard), detects real keyboard visibility (soft-keyboard /
keyboardShown), and syncs autocorrect through typed-text. Hiding the keyboard or iOS dismissing it blurs the remote field when appropriate; cursor-shape tap-keyboard remains as fallback when the stream is off or stale.Extensive Go and Bun tests cover merge logic, CDP connection edge cases, keyboard state machines, and simulated iOS overlay flows.
Reviewed by Cursor Bugbot for commit 6966a49. Bugbot is set up for automated code reviews on this repo. Configure here.