#3812 · Plugin overlays do not signal native browser visibility
GitHub issue · Trusted origin/main: 5aca5733a53a0079a5636227c12e57c3fc03aa53
Verdict: PARTIALLY REPRODUCED · Root-cause confidence: high for the hook disconnect; native visual symptom unverified.
1. TL;DR
The plugin UI hook does not tell BB that an overlay is open. A test using the real host counter shows that opening a host overlay changes its visibility signal, while opening a plugin overlay does not. The Browser tab uses this signal to hide its native view, so the missing update explains the reported overlap. This investigation verifies the hook behavior twice, but does not claim an end-to-end reproduction of native window stacking.
2. Claims vs findings
| Claim | Finding | Evidence |
|---|---|---|
| The plugin hook is inert. | Verified | Real plugin hook leaves the host visibility signal unchanged in both runs; registry payload matches that source. |
| Host overlays hide the native browser. | Verified at hook/code level | Host control test passes; BrowserTabContent gates native visibility on this signal. |
| Plugin app overlays have no automatic hiding policy. | Verified in source | PluginAppOverlays mounts components; SDK contract explicitly leaves visibility policy to the plugin. |
| A menu is obscured by a native page, but works over file tabs and in web. | Unverified visually | No isolated native desktop session was launched; no issue attachments or third-party plugin code were fetched. |
3. Environment
macOS / Darwin arm64; Node 22.22.3; pnpm 9.15.0 via Corepack; Vitest 4.1.1 with jsdom. Fresh clone of public get-bb/bb, fetched origin/main, plus a second clean detached worktree at the same full commit. The system pnpm shim was broken, so a temporary PATH shim delegated to Corepack. Frozen install succeeded; full Turbo build passed 58/58 tasks. No dependency was added.
No BB app/server/provider was started. No runtime data, ports, credentials, or real user instance were used for reproduction. The installed dependency tree did not contain the Electron executable; native desktop launch and screenshot capture were not completed. This is a hook-level partial reproduction, with zero screenshots.
4. Minimal reproduction
Use the trusted commit and save the test artifact at the indicated path:
git clone --branch main https://github.com/get-bb/bb.git bb-3812 cd bb-3812 git checkout --detach 5aca5733a53a0079a5636227c12e57c3fc03aa53 pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build # Copy the linked test to apps/app/src/hooks/issue-3812.test.tsx. cd apps/app pnpm exec vitest run --config vitest.config.ts src/hooks/issue-3812.test.tsx
The direct Vitest command is a focused investigation bypass of the normal Turbo test orchestration; the preceding full build uses Turbo. The normal app Vitest configuration classifies this jsdom file as isolated.
The probe executes the production host and plugin hooks in a fresh Jotai store and reads the real host counter. It does not render BrowserTabContent or Electron. The expected signal is hidden when the plugin hook is active; the actual signal is visible.
AssertionError: expected 'visible' to be 'hidden' // Object.is equality Expected: "hidden" Received: "visible" Tests 1 failed | 1 passed (2)
The failure is intentional evidence of the defect; the host open/close control passes. No production code was changed.
Complete reproduction test
// @vitest-environment jsdom
import { afterEach, expect, test } from "vitest";
import { cleanup, render, screen } from "@testing-library/react";
import { createStore, Provider } from "jotai";
import {
useBrowserDimmingOverlay,
useIsBrowserDimmingModalOpen,
} from "./useBrowserDimmingModal";
import { useBrowserDimmingModal as usePluginDimming } from "../../../../packages/shared-ui/src/hooks/useBrowserDimmingModal";
function Probe({ active, plugin }: { active: boolean; plugin: boolean }) {
useBrowserDimmingOverlay(active && !plugin);
usePluginDimming(active && plugin);
const hidden = useIsBrowserDimmingModalOpen();
return <output data-testid="visibility">{hidden ? "hidden" : "visible"}</output>;
}
afterEach(cleanup);
test("host overlay hides the native view and restores it on close", () => {
const store = createStore();
const view = render(<Provider store={store}><Probe active={false} plugin={false} /></Provider>);
expect(screen.getByTestId("visibility").textContent).toBe("visible");
view.rerender(<Provider store={store}><Probe active={true} plugin={false} /></Provider>);
expect(screen.getByTestId("visibility").textContent).toBe("hidden");
view.rerender(<Provider store={store}><Probe active={false} plugin={false} /></Provider>);
expect(screen.getByTestId("visibility").textContent).toBe("visible");
});
test("plugin registry overlay must hide the same native view while open", () => {
const store = createStore();
const view = render(<Provider store={store}><Probe active={false} plugin={true} /></Provider>);
expect(screen.getByTestId("visibility").textContent).toBe("visible");
view.rerender(<Provider store={store}><Probe active={true} plugin={true} /></Provider>);
expect(screen.getByTestId("visibility").textContent).toBe("hidden");
});
5. Root cause
The host hook owns a module-local Jotai counter, increments it while active, and decrements it on cleanup. The plugin hook has an empty body and returns false from its reader. The registry payload distributes that implementation.
The app build seam substitutes the functional host hook for shared UI imports inside the host build. Vendored plugin registry files retain the inert variant. Dialog invokes the hook with its open state, but that invocation cannot affect the host counter in a plugin.
BrowserTabContent includes !isBrowserDimmingModalOpen in its native view visibility condition. The desktop manager adds a WebContentsView to the host window and sets native visibility. Thus the missing plugin signal leaves the native view eligible to remain visible. Native occlusion is inferred from this path, not captured in this investigation.
PluginAppOverlays supplies a mount boundary without registering dimming. The SDK overlay contract leaves visibility and interaction policy to plugins. Source inspection of the SDK app exports and contract found no dimming registration API.
6. Proposed fix and automation decision
Design a host-backed experimental SDK overlay lifecycle API, bridge it to the existing counter, and update the plugin registry/shared UI hook. Preserve reference counting, nested overlays, close/unmount cleanup, and failure cleanup; add native desktop coverage. Register and document the public API under the repository’s experimental API policy.
No fix PR: exposing this lifecycle is a public SDK/interface decision spanning the host runtime, SDK, registry and API documentation. That exceeds this automation’s simple-fix conditions, including the prohibition on public protocol changes and architecture decisions. The report is also partial, so the full reproduction gate for an automated fix is not met. No fix branch was pushed.
7. Verification
The same agent created a second clean detached worktree at 5aca5733a53a0079a5636227c12e57c3fc03aa53, verified a clean git status before adding the test, ran a separate frozen install, copied only the newly authored test, and repeated the exact Vitest command. It again produced one passing host control and one failing plugin assertion, with the same expected/actual values. No ports or data directories were needed. This verifies the partial hook-level finding; it is not independent review. No report correction was necessary.
8. Related issues and PRs
GitHub cross-reference metadata contained no linked pull request, and an open-PR search for 3812 returned none. An issue search found other browser extensibility work but no demonstrated duplicate of this counter disconnect. No linked branches or third-party code were checked out.
9. Appendix
Checkout paths in logs are replaced with neutral checkout labels. Source permalinks were checked at the recorded commit. Issue suggestions and links were treated as untrusted claims; no instructions, scripts, attachments, or external URLs from the issue were followed.