Keep a keystroke typed into a Bot's browser out of the app's own shortcuts - #602
Merged
Merged
Conversation
…tcuts While somebody drives a Bot's browser, LiveScreen forwards every keydown from a window listener and prevents its default. The app's shortcuts listen on the same window: useHotkey for Shift+N, bound by AppHotkeys when the signed-in app mounts, and SidebarProvider for Ctrl/Cmd+B. Both were bound before LiveScreen, so both saw each keystroke first, and neither looks at defaultPrevented. So a capital N typed into the Bot's browser, the first letter of "New York" in a search box, navigated to /channel/new and took the person away from the Bot mid-word, and Ctrl+B, bold in a document, toggled the local sidebar as well as reaching the remote page. LiveScreen now listens for keydown in the capture phase and stops a keystroke it forwards, so nothing else on this page acts on it. Escape and the paste shortcut return before that, as before: Escape still closes the view and the paste event still fires locally. Keyup is unchanged; no shortcut listens for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kevin9327
requested review from
MikeRyanDev,
davidmckayv,
guidovizoso,
mxmzb and
tylerslaton
as code owners
September 18, 2026 10:25
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
9 tasks
# Conflicts: # CHANGELOG.md
davidmckayv
approved these changes
Sep 18, 2026
davidmckayv
left a comment
Contributor
There was a problem hiding this comment.
Code-verified clean; CI green on this sha.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
While somebody drives a Bot's browser,
LiveScreen(app/src/components/computer/live-screen.tsx) forwards every keystroke from akeydownlistener onwindowand callspreventDefault. The app's own shortcuts listen on the samewindow:useHotkey(app/src/lib/hotkeys/use-hotkey.ts) handles Shift+N → New chat.AppHotkeysbinds it when the signed-in app mounts in_authed.SidebarProvider(app/src/components/ui/sidebar.tsx) handles Ctrl/Cmd+B → toggle sidebar.Both are registered before
LiveScreen, which only binds once driving starts, and listeners on one target run in the order they were added. So both saw every forwarded keystroke first. Neither checksdefaultPrevented, anduseHotkey's editable-field check does not help here, because while driving the keystroke's target is the body or the button that started driving. The result:/channel/new. That unmounts the conversation and the live screen, so the person is taken away from the Bot in the middle of a word.After this change,
LiveScreenlistens forkeydownin the capture phase and callsstopPropagationon a keystroke it forwards. A capture listener onwindowruns before any bubble listener, so a forwarded keystroke reaches the Bot's browser and nothing else on the page. Every existing exception still happens first and returns before the stop:computer-view.tsx's own listener.pasteevent that carries the clipboard text (Paste into a Bot's browser on a layout that does not write Latin letters #596).keyupis unchanged, because no shortcut listens for it. Nothing changes while nobody is driving:LiveScreenbinds no key listener then.This is a single fix in
LiveScreenrather than adefaultPreventedcheck in each shortcut. That way a shortcut added later cannot bring the problem back.Where it runs
keydownlistener moves to the capture phase.Boundary and audit
Changelog
CHANGELOG.mdunderUnreleased.Proof
app/tests/live-screen-app-shortcuts.test.tsxrenders the signed-in shell's pieces with a memory router:SidebarProvider,AppHotkeysand a route showingLiveScreen(with the WebSocket double fromlive-screen-keyboard.test.tsx), plus a/channel/newroute. Keystrokes are dispatched atdocument.body, where a browser sends them when no field has focus.a capital N typed into the Bot's browser goes to it, and does not start a new chat: the path stays/, and the socket gets the Shift+N keydown.Ctrl+B typed into the Bot's browser goes to it, and leaves the sidebar alone: the sidebar stays open, and the socket gets the Ctrl+B keydown.the same shortcuts still work while nobody is driving: Ctrl+B closes the sidebar, and Shift+N navigates to/channel/new. This passes onmaintoo and pins that the shortcuts are unaffected outside driving.With
live-screen.tsxfrommain(only the test added):With the change, all 3 pass, and so do the 9 existing tests in
live-screen-keyboard.test.tsx: the paste shortcut on Latin, Russian, Greek and Dvorak layouts, its keyup, and the keys that are forwarded. Escape returns on the handler's first line, before the new call, and that line is unchanged.bun test --coverageshows all three changed lines executed (thestopPropagationcall and the two listener registrations).App suite (
bun test app, Windows): 892 pass, 29 fail. The 29 failures are exactly the onesmainhas on this machine (serve port, WebSocket, path separator, locale).bun run typecheckinapp/is clean, andbiome checkpasses for both files.I checked the listener order in happy-dom, not in a real browser. The capture-before-bubble order it relies on is the standard DOM dispatch order: capture listeners on
windowrun before bubble listeners onwindowfor any event whose target is inside the document.On Linux too (
ubuntu-latest, Bun 1.3.14; a throwaway workflow on my fork, since fork PRs get no CI here):bun test appis 925 pass, 0 fail with all five of my open PRs merged together, andformat:check,lintandtypecheckare green.🤖 Generated with Claude Code