Symptom (founder-reported, 2026-07-04): after #492, clicking the voice selector trigger on claude.ai does nothing — the popover never appears. No console errors.
Root cause: every open runs toggleMenu → refreshMenu → renderMenu. Both fetches (SpeechSynthesisModule.getVoices voicesCache, PreferenceModule.getVoice in-memory preference map) are warm from the constructor's initial render, so they resolve in microtasks. For a trusted click, microtask checkpoints run between listener invocations — so the rebuild (cleanupExistingElements + recreate) lands between the trigger's click listener and the document-level outside-click listener. That listener then checks this.element.contains(event.target) against a detached old button → treats the opening click as outside → display:none in the same tick the menu opened.
Pre-#492 this was masked by the old base refreshMenu's 100ms sleep, which pushed the rebuild past the click's bubble phase. The refactor removed the sleep and left the Claude dropdown as the one surface still doing wipe-and-rebuild.
Why L4 verification missed it: the probe used page.evaluate(el.click()) — synchronous dispatch, no microtask interleaving between listeners. Only trusted input (CDP Input.dispatchMouseEvent / a real user) reproduces it. Reproduced live with a Playwright page.click probe: {display:"none", ariaExpanded:"false", items:8} with zero console errors.
Fix: make the trigger button and menu container stable nodes — create once, update/refill in place (replaceChildren), never rebuild in renderMenu. This is the refactor's own reconciliation principle applied to the dropdown surface.
Symptom (founder-reported, 2026-07-04): after #492, clicking the voice selector trigger on claude.ai does nothing — the popover never appears. No console errors.
Root cause: every open runs
toggleMenu → refreshMenu → renderMenu. Both fetches (SpeechSynthesisModule.getVoicesvoicesCache,PreferenceModule.getVoicein-memory preference map) are warm from the constructor's initial render, so they resolve in microtasks. For a trusted click, microtask checkpoints run between listener invocations — so the rebuild (cleanupExistingElements+ recreate) lands between the trigger's click listener and the document-level outside-click listener. That listener then checksthis.element.contains(event.target)against a detached old button → treats the opening click as outside →display:nonein the same tick the menu opened.Pre-#492 this was masked by the old base
refreshMenu's 100ms sleep, which pushed the rebuild past the click's bubble phase. The refactor removed the sleep and left the Claude dropdown as the one surface still doing wipe-and-rebuild.Why L4 verification missed it: the probe used
page.evaluate(el.click())— synchronous dispatch, no microtask interleaving between listeners. Only trusted input (CDPInput.dispatchMouseEvent/ a real user) reproduces it. Reproduced live with a Playwrightpage.clickprobe:{display:"none", ariaExpanded:"false", items:8}with zero console errors.Fix: make the trigger button and menu container stable nodes — create once, update/refill in place (
replaceChildren), never rebuild inrenderMenu. This is the refactor's own reconciliation principle applied to the dropdown surface.