Repository navigation
feat: add ⌘K / Ctrl+K command palette to admin UI (#1010) - #1094
Conversation
- Extract shared navigation items and settings sections into packages/frontend/src/lib/navigation.ts - Import shared navigation definitions in app-sidebar.tsx, settings/layout.tsx, and command-palette.tsx - Implement CommandPalette component on @radix-ui/react-dialog with 5 grouped sections: Pages, Settings, Connectors, MCP servers, and Actions - Support case-insensitive matching across query words, labels, keywords, and descriptions with maximum 8 results per group - Cache connectors and MCP servers on first open and invalidate across organization switches - Enforce role-based access: admin-only settings only for ADMIN, hide "Add connector" for VIEWER, and hide SSO in Cloud - Implement full WAI-ARIA combobox accessibility (role="combobox", role="listbox", role="option", aria-controls, aria-activedescendant, aria-selected) - Add visible header trigger showing ⌘K on macOS and Ctrl K elsewhere - Add comprehensive Playwright e2e test suite covering keyboard shortcuts, navigation, permissions, and accessibility Closes HelpCode-ai#1010
helpcodeai
left a comment
There was a problem hiding this comment.
Thanks for this, the shape is right and lib/navigation.ts is now the single source the sidebar and settings layout read from, which was the main thing I wanted. Role and Cloud filtering match the sidebar and there's no new dependency. Two things block it though.
-
The Frontend e2e job didn't just time out, it's a real regression.
command-palette.tsx:219throws(orgs || []).filter is not a functionand takes the whole page down into error.tsx. ThegroupedResultsmemo at:122runs on every AppShell render even with the palette closed, and a lot of the existing specs (billing-awareness, card-trial, footer-links, enterprise-offer, first-success-offer...) stub unknown routes withjson({}), soorgsends up as an object. About 25 tests fail plus retries, hence the 15 min timeout. WithArray.isArray(orgs) ? orgs : []the full suite passes for me locally (181/181), but please also avoid building the result list at all while the palette is closed. Moving the list body into a child component rendered insideDialog.Contentwould do that, and it fixes the query reset below for free. -
Focus doesn't come back after closing. I focused the filter input on /connectors, pressed Ctrl+K, then Esc, and
document.activeElementis<body>. Same when it's opened from the header button. Radix's modal content sends focus to the Dialog.Trigger on close, and there isn't one here. Savedocument.activeElementwhen it opens and refocus it inonCloseAutoFocus(withpreventDefault()). The issue asks for this explicitly, so a test for it would be good too.
Smaller things I'd like fixed in this PR as well:
The reset in handleOpenChange (:108-114) never runs on open, because the shortcut and the header button call setPaletteOpen directly. Close with Esc, reopen, and the old query is still there.
Every page renders its own <AppShell>, so the palette remounts on each navigation and refetches connectors and servers on the first open of every page. A small module-level cache keyed by organizationId would keep them for the session like the issue says. Workspace switches already do a full reload, so you don't need to worry about clearing it there.
On macOS :64 also grabs Ctrl+K, which is "delete to end of line" in Mac text fields. Please only use metaKey on Mac and ctrlKey elsewhere. Ignoring the combo when shift or alt is held wouldn't hurt either.
The adminOnly/selfHostedOnly check is now written twice, in settings/layout.tsx:46-47 and command-palette.tsx:141-145. Could you move it into a helper in navigation.ts that both call? Then the rules can't drift either, which was the whole point of the file. The palette also skips section level adminOnly, which a shared helper would cover.
Optional, but nice: the listbox currently holds plain divs for group headers and the loading row. Use role="group" with aria-labelledby for the groups and put the loading row outside the listbox. Use the connector/server types from lib/api instead of any[]. The SETTINGS_SECTIONS alias at navigation.ts:84 isn't used anywhere. The NAV import in app-sidebar.tsx:42 should go up with the other imports. And skip Enter while isComposing is true.
Last thing: please fill in the PR template, including the commands you ran and their results, plus the keyboard-only walkthrough and the screen reader check from the acceptance criteria.
…i#1094) - Guard against non-array orgs responses with Array.isArray in sidebar and command palette - Lazily render command palette results in CommandPaletteContent inside Dialog.Content - Restore keyboard focus to opening trigger element on close via onCloseAutoFocus - Reset search query to empty on palette open - Cache connectors and MCP servers in module-level cache keyed by organizationId - Add platform-specific shortcut detection (Meta+K on Mac, Ctrl+K elsewhere) and reject Shift/Alt - Centralize shared settings visibility filtering in lib/navigation.ts - Correct listbox ARIA hierarchy with role='group', aria-labelledby, and external status - Export Connector and McpServer interfaces in lib/api.ts - Ignore Enter during IME composition and clean up unused SETTINGS_SECTIONS and import ordering - Add comprehensive regression test suite in command-palette.spec.ts
…vent title truncation (HelpCode-ai#1094)
helpcodeai
left a comment
There was a problem hiding this comment.
Thanks a lot for the thorough follow-up, this is a big step forward! I re-ran everything locally on 14e8dc2: the full Playwright suite (189/189), tsc --noEmit and eslint src are all green, and points 1, 2, 3, 5, 6 and the optional items from the last review look great (focus restoration even works with a plain mouse click and an overlay close).
Two small things left, both in the new per-org cache:
- Failures get cached. In
fetchOrgRecords(command-palette.tsx:53-58), a rejected request becomes[]and goes intorecordsCache, so a single transient 500/timeout hides all connectors/servers until a full page reload. Could you only store the result when both requests succeed (and not cache on.catch)? - The cache is never refreshed. If I open the palette, then create a connector, it never shows up in the palette (and a deleted one stays listed and leads to a 404).
clearCommandPaletteCacheis exported but not called anywhere. The simplest fix is stale-while-revalidate: render the cached list immediately, then refetch in the background each time the palette opens and update it. Please also clear it on logout/org switch, sincelogout()navigates in-app and the module cache survives into the next session in the same tab.
Nits, feel free to take or leave: the desktop header button shows "Search..." but its aria-label is "Open command palette" (WCAG 2.5.3 label-in-name; something like "Search (Ctrl K)" or dropping the aria-label would fix it); e.code === 'KeyK' as a fallback would make the shortcut work on non-Latin keyboard layouts; app-shell.tsx could reuse isMacPlatform(); and isSettingsSectionVisible in lib/navigation.ts is unused.
Finally, could you add a short keyboard-only walkthrough to the PR description (open → type → arrows → Enter → Esc → where focus lands)? A screen-reader pass is welcome if you can do one, but not a blocker. With the two cache fixes we're happy to merge. Thanks again!
…eviewer feedback (HelpCode-ai#1094) - Update fetchOrgRecords to never cache failed requests as empty arrays - Implement stale-while-revalidate caching with immediate render and background revalidation - Clear command palette cache on logout and organization switch - Set header search button aria-label to Search (${shortcutLabel}) for WCAG 2.5.3 compliance - Reuse shared isMacPlatform() helper in AppShell - Add e.code === 'KeyK' fallback for non-Latin keyboard layouts - Remove unused isSettingsSectionVisible helper in navigation.ts - Add regression tests for failed requests, stale-while-revalidate refresh, and logout/org-switch cache clearing
helpcodeai
left a comment
There was a problem hiding this comment.
Thanks a lot for the quick and thorough follow-up, @AYUshJaiswal454! I re-reviewed 7c88480 and everything we asked for is in:
- Failed requests are no longer cached:
fetchOrgRecordsonly writes to the cache when both lists come back as arrays, and a failed refetch keeps the previous list on screen. - Stale-while-revalidate works: the cached list renders immediately and a background refetch on every open picks up new connectors. I checked this with the response delayed by 3s.
- Moving the cache into
lib/command-palette-cache.tsand clearing it fromAuthProvider(logout, rejected session,switchOrg,replaceSession) is a clean fix. Logging out and signing back in within the same tab no longer shows the previous session's records. - Nits done: accessible name now contains the visible "Search",
KeyKfallback, sharedisMacPlatform(), and the unused helper is gone. And thanks for the keyboard-only walkthrough.
Locally: full frontend e2e suite 211/211, tsc clean, eslint 0 errors (same 26 warnings as main).
Two small, non-blocking follow-ups if you feel like it (fine as a separate PR):
- A request still in flight at logout can resolve after
clearCommandPaletteCache()and write its result back intorecordsCache, so the next session in the same tab and org briefly sees the old list until the refetch replaces it. A generation counter in the cache module (increment it inclearCommandPaletteCache, and onlysetif the counter hasn't changed since the request started) would close that. - The "clears command palette cache on logout action" test only checks the redirect to
/login, so it would still pass without the clear. Asserting that an old connector does not show after signing back in (with the connectors route left unanswered) would make it a real regression test.getCachedOrgRecordsis also exported but unused.
Approving. Thanks again for the careful work on this!
Summary
Adds a Command Palette (⌘K / Ctrl+K) to the admin UI with grouped navigation and actions, addressing PR review feedback and fixing mobile header layout.
Changes
Search (${shortcutLabel})for WCAG 2.5.3 (label in name)Type
Testing
Automated test suites run and verified locally:
npm run test:e2e: Passed (193/193 tests passed across 28 spec files in 2.4m)npx playwright test tests/e2e/mobile.spec.ts: Passed (20/20 tests passed)npx playwright test tests/e2e/command-palette.spec.ts: Passed (23/23 tests passed)npx tsc --noEmit -p tsconfig.json: Passed (0 errors)npm run lint: Passed (0 errors, 26 pre-existing warnings in unrelated files)npm run build: Passed (Next.js production build succeeded)Keyboard-Only Walkthrough
The following keyboard-only workflow is verified via automated Playwright E2E testing (
command-palette.spec.ts):⌘K(macOS) /Ctrl+K(other platforms) anywhere in the application, orTabto the header Search button and pressEnter/Space. The palette dialog opens with the search input automatically focused.audit,stripe,dark). The list filters instantly across Pages, Settings, Connectors, MCP servers, and Actions.ArrowDownandArrowUpto cycle through items. The active item updates visually and updatesaria-activedescendanton the combobox.Enteron the selected item to trigger navigation or execute an action (e.g., toggles theme, navigates to/logs, switches organization, logs out).Escapeat any time to dismiss the palette dialog.Escape, overlay click, or execution), keyboard focus is restored back to the opener element (e.g., the header search button or the previously focused input) usingonCloseAutoFocuswithpreventDefault().(Note: Automated Playwright accessibility/ARIA attributes and keyboard flow tests passed; no manual VoiceOver or screen-reader testing was physically performed).
Checklist
npm test)Related Issues
Closes #1010