Skip to content

fix(hud): dismiss the HUD popovers when the window loses focus - #450

Merged
EtienneLescot merged 1 commit into
mainfrom
claude/fix-435-hud-menu-dismiss
Aug 21, 2026
Merged

fix(hud): dismiss the HUD popovers when the window loses focus#450
EtienneLescot merged 1 commit into
mainfrom
claude/fix-435-hud-menu-dismiss

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

What

A HUD popover — the language menu or the device-settings panel — stays open when you click anywhere outside the HUD's own window, and cannot then be dismissed by any means except finding its trigger again.

Fixes #435.

One listener:

window.addEventListener("blur", closePopovers);

Root cause

The HUD's native window is ~904×698, centred at the bottom of the primary display. A click that lands beyond that rectangle reaches this renderer as nothing at all — there is no DOM pointerdown to hit-test, so no renderer-side listener can ever see it. The same click takes keyboard focus away, which makes Escape undeliverable here too. The popover is then stuck: both dismiss paths the component owns are, by construction, unreachable from that state.

blur is the one signal that does cross. The renderer receives it — measured on a packaged build, not assumed — and nothing anywhere in the app acted on it: grep for a blur listener over src/components/launch/ and electron/ returns none, and createHudOverlayWindow never wires BrowserWindow's.

Closing on blur removes the state rather than patching its symptoms: after this, "a popover is open in a window that has no keyboard focus" cannot occur, so the two existing paths only ever have to work while the window is focused — which they do.

Bubble phase is deliberate. Element blur does not bubble, so a window-level bubble listener fires only for the window itself and never while focus moves between the menu's own buttons. Capture phase would have broken keyboard navigation inside the menu.

Escape is not broken — and the issue title used to say it was

Worth stating plainly, because this branch was reviewed once against the old title and refused for not touching the Escape path. It should not touch it.

handleEscape works. A maintainer pressing Esc on a real keyboard closes the menu. Two automated passes concluded otherwise; both were measuring their own tooling.

Settled with a probe attached to the HUD renderer over the remote debugging port — observation only, recording every keydown in capture, bubble and document-capture phases, then driving the HUD with the same synthetic input:

Key sent Recorded by the renderer
a yes — all three phases, isTrusted: true, hasFocus: true, target BUTTON
Escape, immediately after nothing at all

So synthetic keys reach the HUD and Escape specifically is swallowed upstream of the app. The control key is what makes that reading conclusive: without sending a alongside, the same data reads as "no key gets through".

The issue has been re-titled to the behaviour that does reproduce.

Verification

Measured on the packaged 1.9.6 build with real OS input, LaunchWindow.tsx / HudControls.tsx byte-identical to main:

  • click inside the HUD window but outside the menu → closes (the pointerdown path is fine)
  • click beyond the window rect → stays open, and Escape cannot recover it — the bug
  • HUD ex-style 0x00200000 with a popover open, i.e. WS_EX_TRANSPARENT cleared, confirming the window really is input-opaque at that moment and a click reached it rather than passing through

Gates on the rebased branch (origin/main @ 1cc63df4):

npx tsc --noEmit                          exit 0
npx tsc -p tsconfig.test.json --noEmit    exit 0
npx biome check <both files>              no findings
npx vitest --run src/components/launch    8 files, 100 tests passed

Eight new tests in LaunchWindow.test.tsx cover all three dismiss paths for both surfaces, and that Escape leaves the locale unchanged. They are jsdom-dispatched, so per AGENTS.md they pin renderer wiring and prove nothing about real reachability on a click-through window. They are necessary, not sufficient; the real-input results above are the other half.

What this does not cover

  • macOS. The renderer blur was measured firing on Windows. Nobody has checked that it fires when a click lands outside the HUD on macOS, or on a Spaces switch. If it turns out not to, the belt-and-braces fix is a win.on("blur") in createHudOverlayWindow forwarded over IPC — deliberately not added here, since the renderer event demonstrably arrives on the platform this was measured on and a second code path for a signal that already comes through is not free.
  • One behaviour change, named rather than buried. "Check for updates" in the device-settings panel opens a native message box that electron/main.ts parents to the focused window. A parented modal blurs its owner, so that panel now closes behind the dialog instead of staying open underneath it. The dialog carries the verdict and the panel is one click away, so this seems acceptable — but it is a real difference.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Language and device-settings menus now close when the launch window loses focus.
    • Menus consistently dismiss when pressing Escape or clicking outside.
    • Interactions inside the language menu no longer close it unexpectedly.
    • Language selection remains unchanged when dismissing the menu.
    • Other keyboard keys no longer trigger unintended menu dismissal.

The language menu and the device-settings panel already close on Escape
and on a pointerdown outside them, and both of those still work — six of
the eight new tests pass against the unpatched component. What was
missing is the case the issue actually describes.

The HUD's native window is only about 904x698, bottom-centred, and
mostly invisible reserve. A click anywhere else on screen never reaches
this renderer as a pointerdown, so nothing hit-tests it and the popover
stays open — while that same click takes keyboard focus away, after
which Escape cannot be delivered to this renderer either. The menu is
then stuck until the trigger button is found and pressed again, which is
exactly what #435 reports for both halves.

`blur` is the one signal that does cross that boundary, so close the
pair on it. Escape itself needs no change: it already works whenever the
window has focus, and after this a popover can only be open in a window
that has focus. Registered in bubble phase on purpose — element blur
does not bubble, so this never fires while focus moves between the
menu's own buttons.

Adds the dismissal coverage that was missing entirely: Escape (and that
it does not change the locale), outside pointerdown, inside pointerdown,
window blur, and a non-Escape key, for the language menu and the
device-settings panel alike.

Fixes #435
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 43d9a1d8-4153-4d52-9eaa-2432874aac02

📥 Commits

Reviewing files that changed from the base of the PR and between cbbc449 and 5a2e09a.

📒 Files selected for processing (2)
  • src/components/launch/LaunchWindow.test.tsx
  • src/components/launch/LaunchWindow.tsx

Included review availability: Your plan provides up to 4 included reviews per hour; 1 remains after this review.


📝 Walkthrough

Walkthrough

Changes

HUD popover dismissal

Layer / File(s) Summary
Window blur dismissal
src/components/launch/LaunchWindow.tsx
The launch window closes open popovers when the native window loses focus. The blur listener is removed during cleanup.
Dismissal behavior tests
src/components/launch/LaunchWindow.test.tsx
Tests cover Escape, outside pointerdown, window blur, internal pointerdown, unrelated keys, locale preservation, and mock reset behavior.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 5a2e0

The change dismisses HUD popovers when the window loses focus and includes focused test coverage; no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: claude

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 4 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: dismissing HUD popovers when the window loses focus.
Description check ✅ Passed The description thoroughly explains the bug, root cause, implementation, testing, platform scope, and behavior change, despite not using every template heading.
Linked Issues check ✅ Passed The changes address issue #435 by dismissing popovers on window blur and testing Escape dismissal without changing the locale.
Out of Scope Changes check ✅ Passed The listener, tests, comments, and test-reset update directly support HUD popover dismissal and the requirements in issue #435.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-435-hud-menu-dismiss

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@EtienneLescot
EtienneLescot merged commit 54e1270 into main Aug 21, 2026
18 checks passed
@EtienneLescot
EtienneLescot deleted the claude/fix-435-hud-menu-dismiss branch August 21, 2026 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: HUD popovers stay open when a click lands outside the HUD window

1 participant