Repository navigation
🤖 fix: Escape closes the agent picker, sidebar drawer and notifications popover - #5736
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1af03c8c97
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
1af03c8 to
c513991
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c51399154f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
c513991 to
4062495
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 40624951a9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…fication repros A Radix DismissableLayer ignores Escape until it has re-rendered after registering and the re-render's passive effects have run. CI sent Escape inside that window. The repros now arm an in-page probe (a stand-in React DevTools hook plus the layer's fiber) and press Escape once, when the probe reports ready.
Escape timing in the repros: evidence for the test-only pushHead: The window exists on mainSame fixture ( In-page probe, 3 rounds per target. "Lost" means the popover stayed open after Escape.
Repro timing (real tap, wait for visible content, one CDP Escape), 10 tries per target:
On both builds, every lost press had the probe at "not ready" at keydown. Every press that closed the popover had it at "ready". Mutation checkI made the notifications popover ignore Escape ( Green runs on the new head
Generated with |
Readiness: ready with tracked follow-upsHead:
Generated with |
Summary
Escape now closes three layers that ignored it: the agent picker opened with Ctrl+Shift+A, the narrow-screen sidebar drawer, and the notifications popover (which used to reopen as a duplicate tooltip). A click on the notifications bell now only opens its settings and no longer changes them.
Fixes #5676
Fixes #5685
Fixes #5691
Refs #5701 (dropped from this PR, see Scope below)
Background
All of them came from the agent bug bash in #5670.
openPickerfocused the list in arequestAnimationFrame. When the shortcut opened the picker, that callback could run before the list mounted, so focus stayed in the composer. Escape is handled only on the list, so nothing closed it. On main it failed on every second open.onClickalso toggled "Notify on all responses".Implementation
flushSyncand focuses the list right away. The frame-deferred focus stays, because the command palette restores focus to the composer after its action runs.useEscapeToDismiss(enabled, onDismiss)hook for the drawer. It listens in the window's bubble phase, so open layers handle Escape first. Radix popovers, menus and dialogs stop Escape in the document capture phase. React handlers stop it withstopKeyboardPropagation. Document listeners callpreventDefault. The hook also skips IME composition, modified Escape, terminals, remote desktops and open modal dialogs. 🤖 fix: agent bug bash (make bug-bash) and the bugs it found #5670 tried a capture-phase listener and reverted it, because that one took Escape from every popover, dialog and edit mode.useAIViewKeybinds) is in the same phase and mounts first. It now yields Escape while an overlay is open (isEscapeDismissOverlayOpen()), so one Escape never closes an overlay and also stops a stream. When no overlay is open, the hook has no listener, so Escape behaves exactly as on main. Tests cover both cases: a plain composer,body, and a composer that opts in withdata-escape-interrupts-stream.Scope
The tutorial's Escape (#5701) is not in this PR. Three review rounds each found a new ordering case between the tutorial and the layers under it (the drawer, opened before or after the tutorial, and a picker opened by shortcut beneath the tutorial). Each fix needed more ordering mechanism, so I removed the tutorial part instead of growing it. #5701 stays open. A fix there needs a decision about which layer owns Escape while a tutorial covers the app.
Known limitation: when a tutorial shows over the open drawer, Escape closes the drawer behind the tutorial. On main, Escape does nothing in that case.
LeftSidebarnow subscribes to its(max-width: 768px)media query. It used to read the query once per render, so a window narrowed into the drawer width on the new-workspace screen did not get Escape.aria-pressedis gone because the button no longer toggles anything. The docs page says to check the box.Validation
Pre-fix failures (unit tests on main):
New bug-bash repros in
tests/bugbash/repros/escapeClosesOverlays.e2e.ts(4 tests, web and phone targets). I proved each one by restoring the old code in a scratch build. Each failed on its own assertion and passed after the restore:ASSERTION_FAILED: expect.not.toBeExpandedASSERTION_FAILED: expect.toHaveCountASSERTION_FAILED: expect.not.toBeCheckedASSERTION_FAILED: expect.toHaveCountThe repros that read the bell's
aria-pressed(notificationsShortcut,knownFailureDetailsShortcuts,creationFormForgetsName) now read the popover's checkbox through a shared helper.knownFailureDetailsShortcutskeeps itsknown-failuretag for #5671, which a later PR fixes. It reads the setting only at the end, because opening the bell popover would move focus off the details button. It still fails on its own assertion (expect.toBeChecked), andmake test-bugbash-known-failuresfails the same tests as on main.tests/ui/compactionenables notifications with the shortcut, because Radix popover content does not render in happy-dom.Mutation check: with the interrupt's overlay check removed,
useEscapeToDismiss.test.tsxfails.Escape timing in the repros
A Radix popover ignores Escape for a short time after it opens. This window exists on main, too. The CI run on 387b350 sent Escape inside it, so
Test / E2E (linux 1/4)failed. The repros now press Escape exactly once, after an in-page probe intests/bugbash/repros/helpers.tsreports that the layer takes Escape. The probe needs no sleep, no retry and no product change.Why Radix ignores the early Escape (
@radix-ui/react-dismissable-layer1.1.11): the layer registers in an effect (context.layers.add(node)), then sendsdismissableLayer.update. It finds its ownindexonly on the re-render that this event forces (force({})). Its Escape listener calls the handler from the last committed render, and a passive effect installs that handler. Until thenindexis -1, and Escape does nothing.The probe proves each step:
dismissableLayer.update, the probe checks thatcontext.layersholds the popover element, and records the layer's force state.onPostCommitFiberRoot, which React calls after a commit's passive effects have run. The probe reports "ready" the first time the force state has changed there.If Radix or React changes the internals that the probe reads, the wait fails with a named error and does not race.
notificationsShortcut.e2e.tsfailed in CI for the same reason, although this change does not edit it. It callsexpectNotifyOnAllResponsesthree times, and CI failed in that helper (helpers.ts:98,expect(setting).toBeHidden()right after its Escape). The helper now waits for the probe before its Escape. Ctrl+Shift+Comma opens no layer, so the shortcut presses need no wait.Screenshots
Drawer at 390 px, Escape. Before:
After (the drawer was open before Escape as in this shot, then closed):
Bell after Escape. Before, a second copy of the settings:
After, a plain label:
Bell popover at 390 px:
Risks
Low. Escape routing changes only while the narrow drawer is open. During that time Escape closes the overlay instead of interrupting a stream. Users who click the bell to toggle notifications now need the checkbox or the shortcut.
Generated with
xum• Model:anthropic:claude-opus-5-5• Thinking:high