← reports

#3184 · Browser-focused host shortcuts are filtered out

Bug Priority: Medium Effort: Medium desktop ui open on GitHub 2026-09-06 · base 6cdb4ba61255

Verdict: PARTIALLY REPRODUCED · Root-cause confidence: high

1. TL;DR

When an embedded page owns focus, Electron sends the keystroke to that page’s separate webContents. BB already intercepts that event, but its resolver only accepts bindings whose context explicitly includes browserFocus. The desktop numbered-thread bindings include mainSurface but not browserFocus, so the resolver returns null and the host command is never dispatched. This was reproduced twice at the exact trusted main commit. The address-field and named third-party-plugin subclaims were not directly executed, so the overall verdict is partial.

2. Claims vs findings

ClaimStatusEvidence
A numbered thread shortcut is lost while embedded page content owns focus.VerifiedThe focused regression test expected thread.jump.1; the production resolver returned null in both clean runs.
The same behavior occurs in the browser address field.UnverifiedThe address input remains inside the host renderer, under data-app-browser, and therefore takes a different event path than embedded page content. No native desktop interaction was used in this unit-level investigation.
A plugin’s page-level listener cannot receive keys focused in embedded page content.Verified staticallyPlugin content scripts run in the BB window, whose shortcut listener is a host-window keydown listener. Embedded content is intercepted from a separate Electron webContents. The public plugin surface has no host-owned shortcut registration contract.
Unmatched embedded-page shortcuts are left alone.VerifiedThe interceptor returns without calling preventDefault() when the resolver returns null; the existing resolver/manager tests passed 63/63.

3. Environment

4. Minimal reproduction

  1. Check out trusted commit 6cdb4ba6125514b7660332cf311037bc09c82b0e.
  2. Save the regression test shown below as apps/desktop/test/issue-3184-browser-shortcut-repro.test.ts.
  3. From the repository root, run:
    pnpm install --frozen-lockfile --prefer-offline
    pnpm exec turbo run build
    pnpm exec turbo run test --filter=@bb/desktop -- --run test/issue-3184-browser-shortcut-repro.test.ts

Expected: the native resolver returns thread.jump.1, allowing the owning app renderer to dispatch the command.

Actual in both clean runs:

FAIL  test/issue-3184-browser-shortcut-repro.test.ts
AssertionError: expected null to be 'thread.jump.1'

- Expected: "thread.jump.1"
+ Received: null

Test Files  1 failed (1)
Tests       1 failed (1)

Regression test

import type { AppKeybindings } from "@bb/domain";
import { describe, expect, it } from "vitest";
import { resolveDesktopBrowserAppCommand } from "../src/desktop-browser-shortcuts.js";

const keybindings: AppKeybindings = [
  {
    command: "thread.jump.1",
    desktopOnly: true,
    shortcut: {
      key: "1",
      mod: true,
      meta: false,
      control: false,
      alt: false,
      shift: false,
    },
    when: { all: ["mainSurface"], none: ["modalOpen"] },
  },
];

describe("embedded browser app shortcuts", () => {
  it("resolves a numbered thread command while browser content owns focus", () => {
    expect(
      resolveDesktopBrowserAppCommand({
        input: {
          altKey: false,
          code: "Digit1",
          ctrlKey: false,
          key: "1",
          metaKey: true,
          shiftKey: false,
        },
        isMac: true,
        keybindings,
      }),
    ).toBe("thread.jump.1");
  });
});

5. Root cause

The embedded browser interceptor correctly receives native key input and asks the configured resolver for a command. It only prevents the page default after a non-null resolution, then sends the command to the owning renderer. See desktop-browser-view.ts lines 618–639 and main.ts lines 2195–2223.

The resolver has a hard gate: it skips any binding whose when.all does not contain browserFocus. See desktop-browser-shortcuts.ts lines 14–27. Numbered desktop thread bindings are generated with mainSurface and desktopOnly, not browserFocus. See app-keybindings.ts lines 73–105 and its numbered-command registration. The observed null follows directly from that mismatch.

The deeper limitation is that main-process resolution knows configured built-in bindings but not whether the renderer currently has a handler that will accept a command. The renderer normally prevents defaults only after a handler returns true; see AppCommandProvider.tsx lines 302–327. The desktop bridge instead sends a closed AppCommandId and does not receive a handled result. Plugin content scripts are documented as app-window code that may add a keyboard listener, but no public host-owned shortcut registration exists; see the Plugin Guide surface map and the closed built-in command list.

6. Proposed fix (first principles)

Add a host-owned registration channel that keeps Electron main synchronized with the owning renderer’s currently handleable shortcut chords, including opaque plugin registrations. The embedded before-input-event path should match only that active registry, dispatch the opaque registration without focusing either web contents, and call preventDefault() only for an active match. Built-in commands can use the same registry. This requires a product decision for collision precedence, lifecycle, and plugin API shape, plus changes to the desktop contract and Plugin Guide; a resolver-only exception would not meet the handled-only or plugin requirements.

7. Related issues

8. Appendix

Verification

The same agent created a second detached, clean checkout at the recorded base commit, installed with the frozen lockfile, built the monorepo, copied only the reproduction test, and ran the same Turbo command. It failed with the same expected/received values. No report claim was corrected after the second run.

Additional checks

gh api repos/get-bb/bb/commits/main --jq .sha
6cdb4ba6125514b7660332cf311037bc09c82b0e

pnpm exec turbo run test --filter=@bb/desktop -- --run test/desktop-browser-shortcuts.test.ts test/desktop-browser-view-manager.test.ts
Test Files  2 passed (2)
Tests       63 passed (63)

git log 6cdb4ba6125514b7660332cf311037bc09c82b0e..origin/main --oneline -- <affected paths>
[no output]

The issue text was treated as untrusted input. No commands, links, patches, or code from it were executed.