Skip to content

[bug] Browser panes swallow every app shortcut except ⌘W and ⌘R #6186

Closed

Description

@Misophistful

What happens

Clicking into a built-in browser pane stops the app's keyboard shortcuts from working. ⌘B and ⌘L no longer toggle the side panels. Clicking back onto the app chrome restores them. Only ⌘W and ⌘R keep working while the pane has focus, so every other registered shortcut is dead until you click away.

Steps

  1. Open a workspace with a browser pane.
  2. Click inside the rendered page.
  3. Press ⌘B or ⌘L. Nothing happens.
  4. Click the left or right panel and press again. Works.

Why

Browser panes are WebContentsViews, so the guest page runs in its own renderer process. Superset registers hotkeys as document listeners in the host renderer, and the guest's keystrokes never reach that document.

Main can intercept these through before-input-event, and setupBeforeInput does, but only for two keys (apps/desktop/src/main/lib/browser/browser-manager.ts:247):

const key = input.key.toLowerCase();
if (key === "w") { ... }   // close pane
if (key === "r") { ... }   // reload pane

Everything else falls through to the page.

Suggested fix

The renderer already keeps a complete chord index in registeredAppChords (apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts:145), rebuilt whenever overrides or the keyboard layout change. Push that to main, test each before-input-event against it instead of hardcoding two keys, and forward matches to the host renderer to dispatch. The terminal already makes this same call in-process at terminal-key-event-handler.ts:48, where resolveHotkeyFromEvent decides whether a chord belongs to the app or the PTY.

Two things to get right:

  • Text editing and find must still reach the page. ⌘C, ⌘V, ⌘X, ⌘A, ⌘Z and ⌘F need to go to the guest or you lose copy and in-page search on web pages. Several collide with app bindings, so the exclusion list is a judgement call rather than something to derive from the registry.
  • Match on input.code, not input.key. The handler currently compares the character produced while the renderer matches physical position. On Dvorak with adaptive layout mapping on they agree by luck. With it off, ⌘W in a browser pane fires on a different physical key from everywhere else in the app.

Activity

  1. github-actions commented on Aug 5, 2026

    @github-actions
    Contributor

    Summary

    The report holds up. Browser panes are guest webContents living in their own renderer process, so a keydown while the guest has focus never reaches the host document where react-hotkeys-hook listens. The only interception path is main's before-input-event, and that handler hardcodes exactly two keys, so ⌘B (TOGGLE_WORKSPACE_SIDEBAR) and ⌘L (TOGGLE_SIDEBAR) — and every other renderer-registered chord — fall through to the page. Both of the follow-up points in the issue (the input.key vs input.code mismatch, and the need for an editing/find exclusion list) are also correct as written, and I found two extra details a fix has to handle: the chord index is lossy for ⌘W, and main cannot import the renderer's chord utilities as they're currently laid out.

    Affected code

    • apps/desktop/src/main/lib/browser/browser-manager.ts:247 — setupBeforeInput, the whole interception surface. :253 does input.key.toLowerCase() and handles only w / r; :250-251 bail out on any shift/alt chord and on anything without meta/control.
    • apps/desktop/src/renderer/hotkeys/utils/resolveHotkeyFromEvent.ts:121-157 — buildRegisteredAppChords / registeredAppChords, rebuilt on override, layout, and adaptive-toggle changes. This is the index the issue proposes pushing to main.
    • apps/desktop/src/renderer/hotkeys/hooks/useHotkey/useHotkey.ts:32-49 — every app hotkey registers through useHotkeys, i.e. a host-document listener. Nothing pane-aware.
    • apps/desktop/src/renderer/.../BrowserPane/browserRuntimeRegistry.ts:203-216 and :238-248 — the guest is created as a <webview> with no preload, and registers its webContentsId with main on dom-ready. (Minor terminology correction to the issue: these are <webview> tags, not WebContentsViews. The consequence is identical — out-of-process guest, no host keydown — and before-input-event on the guest webContents is still the correct hook.)
    • apps/desktop/src/lib/trpc/routers/browser/browser.ts:132-158 + .../usePersistentWebview/usePersistentWebview.ts:85-107 — the existing two-event forwarding path (onClosePane, onReloadPane) that a general mechanism would replace.

    Root cause

    Confirmed as described: browser-manager.ts:253-263 is the entire chord vocabulary available to a focused browser pane. There is no guest-side preload forwarding keys and no host-side listener on the <webview> element, so anything the main handler doesn't claim is simply consumed by the page.

    Two clarifications on scope:

    • "Every other registered shortcut is dead" is slightly overstated. Main-process menu accelerators still fire with the guest focused: menu.ts:26 (CmdOrCtrl+O), :17-18 (CmdOrCtrl+/, CmdOrCtrl+,), :16 (CmdOrCtrl+Shift+Q), plus the Edit-menu roles at :47-53. So ⌘, and ⌘/ do work in a browser pane. ⌘O is worse than dead — it triggers File → Open Repo instead of OPEN_IN_APP (registry.ts:684), which is the pre-existing menu/registry shadowing already logged under "Known gaps" in docs/KEYBOARD_SYSTEM.md:147.
    • The input.code point is real and reproducible. bindingToDispatchChord (utils/binding.ts:68-76) only translates logical chords when a layout map is present, and getEffectiveLayoutMap() (stores/keyboardPreferencesStore.ts:49-52) returns null once adaptive mapping is off. With the toggle off on Dvorak, CLOSE_PANE's L("meta+w") (registry.ts:341) matches physical KeyW everywhere in the app, while browser-manager.ts:253 matches whichever physical key prints "w" — Dvorak's Comma. Two different keys for the same action.

    Two things the suggested fix will hit

    1. registeredAppChords is chord → single HotkeyId, and the registry has genuine collisions. resolveHotkeyFromEvent.ts:136 does map.set(chord, id) in Object.keys(HOTKEYS) order, so later definitions win: meta+w resolves to CLOSE_TERMINAL (registry.ts:440), not CLOSE_PANE (:341); meta+f resolves to FIND_IN_CHANGES (:394) over FOCUS_TASK_SEARCH/FIND_IN_TERMINAL/FIND_IN_CHAT. If main forwards the resolved HotkeyId, ⌘W in a browser pane regresses from "close this pane" to "close terminal". Forward the canonical chord string and let the renderer re-dispatch a synthetic KeyboardEvent, so the normal focus-scoped useHotkey registrations resolve it exactly as they do elsewhere. getDispatchChord (hooks/useBinding/useBinding.ts:35) is already documented for building such events, though nothing in the codebase synthesizes one yet — expect to work out event.target/activeElement (the focused element in the host document is the <webview> node), which matters for pane-scoped bindings.

    2. Main can't import the chord helpers where they live now. canonicalizeChord / normalizeToken are pure, but resolveHotkeyFromEvent.ts:1-7 pulls in registry.ts (reads navigator at import time) and keyboardPreferencesStore (zustand persist → localStorage). Move the pure canonicalization into apps/desktop/src/shared/ (a peer of shared/tabs-types.ts, already imported by both sides) and have main build its chord from input.code + modifiers with the same function. Electron's input.code uses DOM code names (KeyW, Digit1, Comma, ArrowUp), so normalizeToken works on it unchanged.

    Also drop the :250-251 guards as part of this — shift/alt chords are a large fraction of the registry (meta+shift+k, meta+alt+left, ctrl+shift+tab), and the meta||control requirement quietly excludes any user override bound to an F-key or bare named key. Let the chord table be the gate rather than modifier heuristics.

    On the exclusion list: agreed it's a judgement call, but it's smaller than the issue implies on macOS. No app binding uses meta+c/v/x/a/z (COPY_PATH is meta+shift+c), so those can't collide and stay with the guest for free. The real collisions are ⌘F (page find vs. five meta+f bindings that are all pane-scoped and meaningless for a browser pane — I'd leave ⌘F with the guest), ⌘P (page print vs. QUICK_OPEN), and ⌘G (find-next vs. RUN_WORKSPACE_COMMAND).

    Reproduction

    Manual:

    1. Open a workspace, ⌘⇧S (SPLIT_WITH_BROWSER) or add a browser pane.
    2. Click inside the rendered page.
    3. ⌘B / ⌘L → nothing; ⌘W / ⌘R → work; ⌘, / ⌘/ → work (menu accelerators).
    4. Click the sidebar, repeat → all work.

    For the input.code half: Settings → Keyboard, turn off adaptive layout mapping, switch the OS input source to Dvorak, then press ⌘W in a browser pane vs. in a terminal pane — the pane closes from different physical keys.

    Unit-level, once the chord builder is extracted to shared/, this is directly testable in bun:test without Electron:

    // shared/browser-pane-chords.test.ts
    const chords = new Set(["meta+b", "meta+l", "meta+w"]); // pushed from renderer
    it("matches ⌘B", () => {
      expect(matchAppChord({ type: "keyDown", code: "KeyB", meta: true }, chords)).toBe("meta+b");
    });
    it("anchors to physical position, not produced character", () => {
      // Dvorak ⌘W: code KeyW, key ","
      expect(matchAppChord({ type: "keyDown", code: "KeyW", key: ",", meta: true }, chords)).toBe("meta+w");
    });
    it("leaves page find to the guest", () => {
      expect(matchAppChord({ type: "keyDown", code: "KeyF", meta: true }, chords)).toBeNull();
    });

    Against today's code the equivalent assertion is that a synthesized ⌘B Input produces no emit from setupBeforeInput — that currently needs an Electron webContents stub since the handler is private and installed in register() (browser-manager.ts:64), which is another argument for pulling the matching decision out into a pure, shared function.

    Worth adding a row to docs/KEYBOARD_SYSTEM.md's "Known gaps" table (:143-155) either way — browser-pane forwarding isn't listed there today, next to the terminal-forwarding entry that solved the same problem in-process.

  2. Misophistful commented on Aug 31, 2026

    @Misophistful
    Author

    I changed my mind. I realised that hijacking the browser keyboard shortcuts isn't a good idea. Many webapps (including the one I'm building with Superset) use keyboard shortcuts, so the current behaviour of passing them through to the browser makes perfect sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions