diff --git a/frontend/src/components/GlobalCommandPalette.tsx b/frontend/src/components/GlobalCommandPalette.tsx index 8bda9cfc..4b8c6cbf 100644 --- a/frontend/src/components/GlobalCommandPalette.tsx +++ b/frontend/src/components/GlobalCommandPalette.tsx @@ -161,6 +161,19 @@ export function GlobalCommandPalette({ navItems, onNavigate, onSelectStack }: Gl placeholder="Search the app..." value={query} onValueChange={setQuery} + // Own the Escape-to-close instead of relying solely on Radix + // Dialog's dismissable layer. While the palette is open the + // cross-node stack search streams results in and re-renders the + // tree; an Escape that lands during that churn was observed to be + // dropped before Radix's layer dismissed it. Closing here via + // handleOpenChange(false) keeps Escape deterministic and is + // idempotent if Radix also dismisses on the same key. + onKeyDown={e => { + if (e.key === 'Escape') { + e.preventDefault(); + handleOpenChange(false); + } + }} /> {showEmptyState && ( diff --git a/frontend/src/components/__tests__/GlobalCommandPalette.test.tsx b/frontend/src/components/__tests__/GlobalCommandPalette.test.tsx index 3c4ee433..3f69f5e5 100644 --- a/frontend/src/components/__tests__/GlobalCommandPalette.test.tsx +++ b/frontend/src/components/__tests__/GlobalCommandPalette.test.tsx @@ -166,6 +166,22 @@ describe('GlobalCommandPalette', () => { }); }); + it('closes the palette when Escape is pressed in the search input', async () => { + renderPalette(); + open(); + const input = screen.getByPlaceholderText('Search the app...'); + fireEvent.keyDown(input, { key: 'Escape' }); + // Behavioral smoke test only: in jsdom Radix's dismissable layer already + // closes a single-layer dialog on Escape, so this cannot isolate the + // palette's own Escape handler from that fallback. The deterministic-close + // regression the handler fixes (Escape dropped during streaming re-renders) + // is guarded by the Playwright command-palette spec. + await waitFor(() => { + const dialog = screen.queryByRole('dialog'); + expect(dialog?.getAttribute('data-state') ?? 'unmounted').not.toBe('open'); + }); + }); + it('disables an offline node row', () => { nodesValue = { nodes: [node(1, 'local'), node(2, 'opsix', 'offline')],