feat: add global keyboard shortcut system (closes #83)#119
Conversation
- Centralized shortcut registry (utils/keyboardShortcuts.ts) covering all shortcuts from the issue: navigation, search, data actions, table nav, globe controls, dashboard controls, utility. - useKeyboardShortcuts hook: single window keydown listener, ignores typing contexts (inputs/textareas/contenteditable/code editors), handles Ctrl/Shift/Alt (+ Cmd on macOS), built-in help modal toggle. - ShortcutHelpModal: categorized cheat sheet styled with Kepler's existing design tokens, closes on Esc/backdrop click. - Wired into App.tsx: nav shortcuts -> real dashboard routes, sidebar toggle -> uiStore, refresh -> React Query cache invalidation. - Telemetry/Space Weather/Risk Assessment/Prediction Analytics have no pages yet, so those shortcuts are defined (visible in the help modal) but intentionally left unwired until those routes exist. Signed-off-by: SankeerthNara <sankeerthnara@gmail.com>
🤖 CodeAnt AI — Review Status
Updated in place by CodeAnt AI · last 5 reviews |
📝 WalkthroughWalkthroughAdds a centralized keyboard shortcut registry and global event hook, wires shortcuts to navigation and UI actions, and introduces a responsive, focus-managed help modal available across application routes. ChangesGlobal keyboard shortcuts
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant GlobalShortcuts
participant useKeyboardShortcuts
participant AppState
User->>useKeyboardShortcuts: Press registered shortcut
useKeyboardShortcuts->>GlobalShortcuts: Dispatch mapped handler
GlobalShortcuts->>AppState: Navigate, refresh queries, open search, or toggle sidebar
Possibly related issues
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
|
||
| // Utility | ||
| { id: 'util-help-slash', combo: { key: '/', modifiers: ['ctrl'] }, description: 'Open shortcut help', category: 'Utility', handlerKey: 'openShortcutHelp' }, | ||
| { id: 'util-help-question', combo: { key: '?' }, description: 'Show shortcut cheat sheet', category: 'Utility', handlerKey: 'openShortcutHelp' }, |
There was a problem hiding this comment.
Question-Mark Shortcut Never Matches
On common layouts such as US keyboards, pressing ? produces key === '?' with shiftKey === true. This definition requests no Shift modifier, so the exact modifier check rejects the event and the help modal does not open.
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/utils/keyboardShortcuts.ts
Line: 127
Comment:
**Question-Mark Shortcut Never Matches**
On common layouts such as US keyboards, pressing `?` produces `key === '?'` with `shiftKey === true`. This definition requests no Shift modifier, so the exact modifier check rejects the event and the help modal does not open.
How can I resolve this? If you propose a fix, please make it concise.| openManeuverPlanning: () => navigate('/dashboard/mission-planner'), | ||
|
|
||
| // Search | ||
| openGlobalSearch: () => setGlobalSearchOpen(true), |
There was a problem hiding this comment.
Pressing Ctrl/Cmd+K prevents the browser default and sets globalSearchOpen, but no rendered component reads that state. The advertised shortcut therefore has no visible effect while also taking over the browser shortcut.
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/App.tsx
Line: 52
Comment:
**Global Search Has No Consumer**
Pressing Ctrl/Cmd+K prevents the browser default and sets `globalSearchOpen`, but no rendered component reads that state. The advertised shortcut therefore has no visible effect while also taking over the browser shortcut.
How can I resolve this? If you propose a fix, please make it concise.| openGlobeView: () => navigate('/dashboard'), | ||
| goToDashboard: () => navigate('/dashboard'), | ||
| goHome: () => navigate('/dashboard'), | ||
| openSatelliteManagement: () => navigate('/dashboard/satellites'), | ||
| openCollisionPrediction: () => navigate('/dashboard/collision-center'), | ||
| openAiAgentDashboard: () => navigate('/dashboard/ai-agents'), | ||
| openManeuverPlanning: () => navigate('/dashboard/mission-planner'), |
There was a problem hiding this comment.
Dashboard Keys Leak Across Routes
GlobalShortcuts is mounted outside the route layouts, so these unmodified letter shortcuts remain active on marketing and authentication pages. Pressing S, G, D, C, A, M, or H while no input is focused unexpectedly leaves the current public page and navigates into the dashboard.
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/App.tsx
Line: 43-49
Comment:
**Dashboard Keys Leak Across Routes**
`GlobalShortcuts` is mounted outside the route layouts, so these unmodified letter shortcuts remain active on marketing and authentication pages. Pressing `S`, `G`, `D`, `C`, `A`, `M`, or `H` while no input is focused unexpectedly leaves the current public page and navigates into the dashboard.
How can I resolve this? If you propose a fix, please make it concise.| useEffect(() => { | ||
| if (!isOpen) return; | ||
| const previouslyFocused = document.activeElement as HTMLElement | null; | ||
| dialogRef.current?.focus(); | ||
| return () => previouslyFocused?.focus(); |
There was a problem hiding this comment.
This effect moves focus into the dialog but does not keep it there. A keyboard user can Tab past the modal controls into interactive content behind the backdrop, even though the dialog declares aria-modal="true".
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/components/ShortcutHelpModal.tsx
Line: 27-31
Comment:
**Modal Focus Escapes Backdrop**
This effect moves focus into the dialog but does not keep it there. A keyboard user can Tab past the modal controls into interactive content behind the backdrop, even though the dialog declares `aria-modal="true"`.
How can I resolve this? If you propose a fix, please make it concise.| // Data Actions — refresh wired via React Query cache invalidation at App level. | ||
| { id: 'data-refresh', combo: { key: 'r', modifiers: ['ctrl'] }, description: 'Refresh current data', category: 'Data Actions', handlerKey: 'refreshCurrentData' }, | ||
| { id: 'data-force-refresh', combo: { key: 'r', modifiers: ['ctrl', 'shift'] }, description: 'Force refresh all live data', category: 'Data Actions', handlerKey: 'forceRefreshAllData' }, | ||
| { id: 'data-export', combo: { key: 'e', modifiers: ['ctrl'] }, description: 'Export current table', category: 'Data Actions', handlerKey: 'exportCurrentTable' }, | ||
| { id: 'data-export-all', combo: { key: 'e', modifiers: ['ctrl', 'shift'] }, description: 'Export all records', category: 'Data Actions', handlerKey: 'exportAllRecords' }, | ||
| { id: 'data-save', combo: { key: 's', modifiers: ['ctrl'] }, description: 'Save settings where applicable', category: 'Data Actions', handlerKey: 'saveSettings' }, |
There was a problem hiding this comment.
Help Lists Inactive Data Actions
The modal renders these entries from the global registry, but App.tsx registers only the two refresh handlers. Ctrl+E, Ctrl+Shift+E, and Ctrl+S therefore match a definition and then silently do nothing, so the cheat sheet advertises actions that are unavailable.
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/utils/keyboardShortcuts.ts
Line: 90-95
Comment:
**Help Lists Inactive Data Actions**
The modal renders these entries from the global registry, but `App.tsx` registers only the two refresh handlers. Ctrl+E, Ctrl+Shift+E, and Ctrl+S therefore match a definition and then silently do nothing, so the cheat sheet advertises actions that are unavailable.
How can I resolve this? If you propose a fix, please make it concise.| { id: 'table-up', combo: { key: 'ArrowUp' }, description: 'Move up a row', category: 'Table Navigation', handlerKey: 'tableMoveUp' }, | ||
| { id: 'table-down', combo: { key: 'ArrowDown' }, description: 'Move down a row', category: 'Table Navigation', handlerKey: 'tableMoveDown' }, | ||
| { id: 'table-left', combo: { key: 'ArrowLeft' }, description: 'Move left a column', category: 'Table Navigation', handlerKey: 'tableMoveLeft' }, | ||
| { id: 'table-right', combo: { key: 'ArrowRight' }, description: 'Move right a column', category: 'Table Navigation', handlerKey: 'tableMoveRight' }, | ||
| { id: 'table-open', combo: { key: 'Enter' }, description: 'Open selected item', category: 'Table Navigation', handlerKey: 'tableOpenSelected' }, | ||
| { id: 'table-clear-filters', combo: { key: 'Delete' }, description: 'Clear filters', category: 'Table Navigation', handlerKey: 'tableClearFilters' }, | ||
| { id: 'table-first-row', combo: { key: 'Home' }, description: 'Jump to first row', category: 'Table Navigation', handlerKey: 'tableJumpToFirstRow' }, | ||
| { id: 'table-last-row', combo: { key: 'End' }, description: 'Jump to last row', category: 'Table Navigation', handlerKey: 'tableJumpToLastRow' }, | ||
| { id: 'table-page-up', combo: { key: 'PageUp' }, description: 'Previous page', category: 'Table Navigation', handlerKey: 'tablePageUp' }, | ||
| { id: 'table-page-down', combo: { key: 'PageDown' }, description: 'Next page', category: 'Table Navigation', handlerKey: 'tablePageDown' }, |
There was a problem hiding this comment.
Table Shortcuts Have No Registration Path
Every table command is displayed in the help modal, but the only hook instance uses the global definitions and supplies none of these handlers. No page registers a scoped shortcut hook, so all ten advertised table shortcuts are reachable no-ops.
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/utils/keyboardShortcuts.ts
Line: 98-107
Comment:
**Table Shortcuts Have No Registration Path**
Every table command is displayed in the help modal, but the only hook instance uses the global definitions and supplies none of these handlers. No page registers a scoped shortcut hook, so all ten advertised table shortcuts are reachable no-ops.
How can I resolve this? If you propose a fix, please make it concise.| { id: 'globe-zoom-in', combo: { key: '+' }, description: 'Zoom in', category: 'Globe Controls', handlerKey: 'globeZoomIn' }, | ||
| { id: 'globe-zoom-out', combo: { key: '-' }, description: 'Zoom out', category: 'Globe Controls', handlerKey: 'globeZoomOut' }, | ||
| { id: 'globe-reset-camera', combo: { key: '0' }, description: 'Reset camera', category: 'Globe Controls', handlerKey: 'globeResetCamera' }, | ||
| { id: 'globe-toggle-labels', combo: { key: 'l' }, description: 'Toggle labels', category: 'Globe Controls', handlerKey: 'globeToggleLabels' }, | ||
| { id: 'globe-toggle-debris', combo: { key: 'b' }, description: 'Toggle debris visibility', category: 'Globe Controls', handlerKey: 'globeToggleDebris' }, | ||
| { id: 'globe-toggle-orbits', combo: { key: 'o' }, description: 'Toggle orbit paths', category: 'Globe Controls', handlerKey: 'globeToggleOrbits' }, | ||
| { id: 'globe-focus-satellite', combo: { key: 'f' }, description: 'Focus selected satellite', category: 'Globe Controls', handlerKey: 'globeFocusSatellite' }, |
There was a problem hiding this comment.
These controls are shown as available in the help modal, but neither the global handler map nor the globe view registers their handler keys. Pressing the advertised zoom, camera, label, debris, orbit, or focus keys therefore produces no globe action.
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/utils/keyboardShortcuts.ts
Line: 110-116
Comment:
**Globe Shortcuts Are Unwired**
These controls are shown as available in the help modal, but neither the global handler map nor the globe view registers their handler keys. Pressing the advertised zoom, camera, label, debris, orbit, or focus keys therefore produces no globe action.
How can I resolve this? If you propose a fix, please make it concise.| { id: 'nav-risk', combo: { key: 'r' }, description: 'Open Risk Assessment', category: 'Navigation', handlerKey: 'openRiskAssessment' }, | ||
| { id: 'nav-prediction', combo: { key: 'p' }, description: 'Open Prediction Analytics', category: 'Navigation', handlerKey: 'openPredictionAnalytics' }, | ||
| { id: 'nav-home', combo: { key: 'h' }, description: 'Return to Home/Dashboard', category: 'Navigation', handlerKey: 'goHome' }, | ||
| { id: 'nav-close-modal', combo: { key: 'Escape' }, description: 'Close open modal or dialog', category: 'Navigation', handlerKey: 'closeModal', allowInTypingContext: true }, |
There was a problem hiding this comment.
The registry advertises Escape as closing an open modal or dialog, but the hook handles this key only when its own help modal is open and App.tsx supplies no general closeModal handler. Other application dialogs remain open when users invoke the advertised shortcut.
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/utils/keyboardShortcuts.ts
Line: 83
Comment:
**Escape Only Closes Help**
The registry advertises Escape as closing an open modal or dialog, but the hook handles this key only when its own help modal is open and `App.tsx` supplies no general `closeModal` handler. Other application dialogs remain open when users invoke the advertised shortcut.
How can I resolve this? If you propose a fix, please make it concise.| }) => { | ||
| const dialogRef = useRef<HTMLDivElement>(null); | ||
|
|
||
| // Keep focus inside the modal while open, restore it on close. |
There was a problem hiding this comment.
Suggestion: This comment states focus is kept inside the modal, but the implementation only focuses the dialog once and restores prior focus on close; it does not trap Tab navigation. Update the comment to match behavior or implement an actual focus trap. [comment mismatch]
Severity Level: Minor 🧹
- ⚠️ Modal comment overstates accessibility behavior versus implementation.
- ⚠️ Developers may assume focus trapping that does not exist.Steps of Reproduction ✅
1. Open the Kepler frontend so that `App` renders, which includes `GlobalShortcuts` and
the `ShortcutHelpModal` component (`frontend/src/App.tsx:34-63`).
2. Trigger the keyboard shortcut help modal using the working shortcut `Ctrl + /`, which
is defined as `util-help-slash` in `SHORTCUT_DEFINITIONS`
(`frontend/src/utils/keyboardShortcuts.ts:126`) and handled by `useKeyboardShortcuts`
(`frontend/src/hooks/useKeyboardShortcuts.ts:91-104`), causing `isHelpOpen` to become
`true` and `ShortcutHelpModal` to render (`App.tsx:63`).
3. When `ShortcutHelpModal` mounts with `isOpen` true, its `useEffect` hook at
`frontend/src/components/ShortcutHelpModal.tsx:26-32` runs: it stores
`document.activeElement` in `previouslyFocused`, calls `dialogRef.current?.focus()` once,
and returns a cleanup that restores the prior focus on close; no further keyboard or
focus-trap logic is implemented in this component.
4. While the modal is open, press `Tab` repeatedly: because there is no additional handler
(e.g., `onKeyDown`, focus-looping, or trapping via `tabIndex` management) in
`ShortcutHelpModal.tsx` beyond the initial `focus()` call, focus is free to move to other
focusable elements outside the modal, demonstrating that focus is not “kept inside the
modal” as the comment describes, so the comment is misleading relative to actual behavior.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** frontend/src/components/ShortcutHelpModal.tsx
**Line:** 26:26
**Comment:**
*Comment Mismatch: This comment states focus is kept inside the modal, but the implementation only focuses the dialog once and restores prior focus on close; it does not trap `Tab` navigation. Update the comment to match behavior or implement an actual focus trap.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| { id: 'table-page-down', combo: { key: 'PageDown' }, description: 'Next page', category: 'Table Navigation', handlerKey: 'tablePageDown' }, | ||
|
|
||
| // Globe Controls — wired inside Dashboard/EarthTwin, not globally. | ||
| { id: 'globe-zoom-in', combo: { key: '+' }, description: 'Zoom in', category: 'Globe Controls', handlerKey: 'globeZoomIn' }, |
There was a problem hiding this comment.
Suggestion: The + shortcut is also missing shift, but + requires Shift on standard keyboards and the hook rejects extra modifiers, so zoom-in will never trigger. Define this shortcut with modifiers: ['shift'] (or adjust matching logic for printable symbols). [incorrect condition logic]
Severity Level: Major ⚠️
- ⚠️ Planned globe zoom shortcut will not work with `+`.
- ⚠️ Future EarthTwin keyboard UX compromised until definition fixed.Steps of Reproduction ✅
1. Inspect the globe controls shortcut definitions in
`frontend/src/utils/keyboardShortcuts.ts:109-117`, which include `{ id: 'globe-zoom-in',
combo: { key: '+' }, description: 'Zoom in', category: 'Globe Controls', handlerKey:
'globeZoomIn' }` at line 110, with no `modifiers` specified on the combo.
2. Observe the comment above these definitions: “Globe Controls — wired inside
Dashboard/EarthTwin, not globally.” (`keyboardShortcuts.ts:109), indicating the intent
that these shortcuts will be used with `useKeyboardShortcuts` in a page-specific context
once the EarthTwin/globe view wiring is added.
3. Examine the shortcut matching logic in `frontend/src/hooks/useKeyboardShortcuts.ts`:
`handleKeyDown` runs through `definitions` and applies `keyMatches(event, def.combo.key)`
(`hooks/useKeyboardShortcuts.ts:91-97) plus `modifiersMatch(event, def.combo.modifiers)`
(`hooks/useKeyboardShortcuts.ts:96-97). `modifiersMatch` computes `wantShift` from the
`modifiers` array and requires `wantShift === event.shiftKey`
(`hooks/useKeyboardShortcuts.ts:46-56), so any pressed modifier that is not listed causes
the match to fail.
4. On a standard keyboard, pressing the `+` key requires `Shift` (usually `Shift + '='`),
yielding a `KeyboardEvent` with `event.key === '+'` and `event.shiftKey === true`. Given
the current `globe-zoom-in` definition has `modifiers` undefined (treated as `[]`),
`keyMatches` would succeed but `modifiersMatch` would compute `wantShift === false` and
fail because `event.shiftKey === true`, preventing any future `globeZoomIn` handler from
ever firing when users press `+` to zoom. This means that once globe zoom-in is wired up
in an EarthTwin page using these definitions, the intended shortcut will be unreachable
until either `modifiers: ['shift']` is added to the combo or the matching logic is relaxed
for symbol keys.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** frontend/src/utils/keyboardShortcuts.ts
**Line:** 110:110
**Comment:**
*Incorrect Condition Logic: The `+` shortcut is also missing `shift`, but `+` requires `Shift` on standard keyboards and the hook rejects extra modifiers, so zoom-in will never trigger. Define this shortcut with `modifiers: ['shift']` (or adjust matching logic for printable symbols).
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix|
|
||
| // Utility | ||
| { id: 'util-help-slash', combo: { key: '/', modifiers: ['ctrl'] }, description: 'Open shortcut help', category: 'Utility', handlerKey: 'openShortcutHelp' }, | ||
| { id: 'util-help-question', combo: { key: '?' }, description: 'Show shortcut cheat sheet', category: 'Utility', handlerKey: 'openShortcutHelp' }, |
There was a problem hiding this comment.
Suggestion: The ? shortcut is defined without shift, but the matcher requires exact modifier parity, so pressing ? (which sets Shift) will never match and the help modal cannot be opened with that key. Add shift to this combo or relax modifier matching for symbol keys. [incorrect condition logic]
Severity Level: Major ⚠️
- ❌ Advertised `?` shortcut fails to open help modal.
- ⚠️ Users rely on help modal for shortcut discovery.Steps of Reproduction ✅
1. Start the app so `App` renders `GlobalShortcuts`, which calls `useKeyboardShortcuts`
with handlers and uses the default `SHORTCUT_DEFINITIONS` (`frontend/src/App.tsx:34-61`,
`frontend/src/hooks/useKeyboardShortcuts.ts:77`).
2. Note that the shortcut definition for the help “?” key is `{ id: 'util-help-question',
combo: { key: '?' }, ... handlerKey: 'openShortcutHelp' }` in
`frontend/src/utils/keyboardShortcuts.ts:127`, with no `modifiers` array specified.
3. Observe the matching logic in `useKeyboardShortcuts`: `handleKeyDown` iterates
`definitions` and, for each `def`, calls `keyMatches(event, def.combo.key)`
(`hooks/useKeyboardShortcuts.ts:94-97`) and `modifiersMatch(event, def.combo.modifiers)`
(`hooks/useKeyboardShortcuts.ts:96-97). `modifiersMatch` computes desired modifiers from
the array and requires exact parity against `event.ctrlKey/metaKey`, `event.shiftKey`, and
`event.altKey` (`hooks/useKeyboardShortcuts.ts:46-57), defaulting `modifiers` to `[]`.
4. With the app focused, press `?` on a standard keyboard (Shift + `/`): the browser
dispatches a `KeyboardEvent` with `event.key === '?'` and `event.shiftKey === true`.
`keyMatches` returns true because the key strings match
(`hooks/useKeyboardShortcuts.ts:60-63), but `modifiersMatch` compares `wantShift` (false,
because `modifiers` is `[]`) to `event.shiftKey` (true) and returns false
(`hooks/useKeyboardShortcuts.ts:48-56). As a result, the `handlerKey ===
'openShortcutHelp'` branch (`hooks/useKeyboardShortcuts.ts:99-103) never executes, the
`isHelpOpen` state is not toggled, and the help modal does not open via `?`, even though
the footer of `ShortcutHelpModal` advertises “Press ? or Ctrl + / anytime to reopen this”
(`frontend/src/components/ShortcutHelpModal.tsx:108-110).(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** frontend/src/utils/keyboardShortcuts.ts
**Line:** 127:127
**Comment:**
*Incorrect Condition Logic: The `?` shortcut is defined without `shift`, but the matcher requires exact modifier parity, so pressing `?` (which sets `Shift`) will never match and the help modal cannot be opened with that key. Add `shift` to this combo or relax modifier matching for symbol keys.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fixThere was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
frontend/src/components/ShortcutHelpModal.tsx (1)
27-32: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winModal has no Tab-key focus trap.
Focus is placed on the dialog when it opens and restored on close, but nothing stops
Tab/Shift+Tabfrom moving focus out into the background page while the modal is open, since only Escape/backdrop-click close it.Proposed fix: trap Tab within the dialog
useEffect(() => { if (!isOpen) return; const previouslyFocused = document.activeElement as HTMLElement | null; dialogRef.current?.focus(); - return () => previouslyFocused?.focus(); + const handleTabTrap = (e: KeyboardEvent) => { + if (e.key !== 'Tab' || !dialogRef.current) return; + const focusables = dialogRef.current.querySelectorAll<HTMLElement>( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + if (!focusables.length) return; + const first = focusables[0]; + const last = focusables[focusables.length - 1]; + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + }; + document.addEventListener('keydown', handleTabTrap); + return () => { + document.removeEventListener('keydown', handleTabTrap); + previouslyFocused?.focus(); + }; }, [isOpen]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/components/ShortcutHelpModal.tsx` around lines 27 - 32, Update the focus-management useEffect in ShortcutHelpModal to add a keydown handler while the modal is open that traps Tab and Shift+Tab within the dialog’s focusable descendants, wrapping forward from the last element to the first and backward from the first to the last. Register and clean up the handler with the existing effect, while preserving focus restoration on close.frontend/src/hooks/useKeyboardShortcuts.ts (1)
18-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider a typed
handlerKeyunion instead of barestring.
ShortcutHandlersaccepts any string key, so a typo in the handlers map passed fromApp.tsx(e.g. misspellingopenGlobalSearch) compiles fine but silently never fires. Deriving a union fromSHORTCUT_DEFINITIONSwould catch this at compile time.Proposed refactor
-export type ShortcutHandlers = Partial<Record<string, () => void>>; +export type ShortcutHandlerKey = (typeof SHORTCUT_DEFINITIONS)[number]['handlerKey']; +export type ShortcutHandlers = Partial<Record<ShortcutHandlerKey, () => void>>;(requires
SHORTCUT_DEFINITIONSto be declaredas constinkeyboardShortcuts.tsfor the literal union to be inferred.)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/hooks/useKeyboardShortcuts.ts` around lines 18 - 25, Replace the bare string key in ShortcutHandlers with a literal handlerKey union derived from the readonly SHORTCUT_DEFINITIONS declaration, ensuring SHORTCUT_DEFINITIONS is declared as const so its handler keys remain literals. Update the related ShortcutDefinition typing as needed while preserving the existing handler lookup and custom definitions behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/App.tsx`:
- Around line 34-64: Scope GlobalShortcuts to dashboard routes by importing and
using useLocation alongside useNavigate, then pass the existing disabled option
to useKeyboardShortcuts when the current pathname is outside /dashboard
(including marketing and authentication pages). Preserve all shortcut handlers
for dashboard routes.
- Around line 39-61: Wire the existing openSettings shortcut in the
useKeyboardShortcuts configuration by navigating to the /dashboard/settings
route, alongside the other navigation handlers. Keep the documented no-op scope
limited to routes that do not yet exist.
In `@frontend/src/utils/keyboardShortcuts.ts`:
- Around line 118-124: Update the dash-toggle-theme shortcut in the keyboard
shortcut definitions to use a non-browser-reserved key combination instead of
Ctrl+T, avoiding both Ctrl+T and Ctrl+Shift+T while preserving its toggleTheme
handler and dashboard-controls metadata.
---
Nitpick comments:
In `@frontend/src/components/ShortcutHelpModal.tsx`:
- Around line 27-32: Update the focus-management useEffect in ShortcutHelpModal
to add a keydown handler while the modal is open that traps Tab and Shift+Tab
within the dialog’s focusable descendants, wrapping forward from the last
element to the first and backward from the first to the last. Register and clean
up the handler with the existing effect, while preserving focus restoration on
close.
In `@frontend/src/hooks/useKeyboardShortcuts.ts`:
- Around line 18-25: Replace the bare string key in ShortcutHandlers with a
literal handlerKey union derived from the readonly SHORTCUT_DEFINITIONS
declaration, ensuring SHORTCUT_DEFINITIONS is declared as const so its handler
keys remain literals. Update the related ShortcutDefinition typing as needed
while preserving the existing handler lookup and custom definitions behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: b2a5a164-daee-40e7-9e74-c8324d3d7828
📒 Files selected for processing (4)
frontend/src/App.tsxfrontend/src/components/ShortcutHelpModal.tsxfrontend/src/hooks/useKeyboardShortcuts.tsfrontend/src/utils/keyboardShortcuts.ts
| function GlobalShortcuts() { | ||
| const navigate = useNavigate(); | ||
| const queryClient = useQueryClient(); | ||
| const { toggleSidebar, setGlobalSearchOpen } = useUIStore(); | ||
|
|
||
| const { isHelpOpen, closeHelp, isMac } = useKeyboardShortcuts({ | ||
| // Navigation — routes that exist today. Telemetry / Space Weather / | ||
| // Risk Assessment / Prediction Analytics have no page yet, so those | ||
| // shortcuts are left unwired (safe no-ops) until those routes land. | ||
| openGlobeView: () => navigate('/dashboard'), | ||
| goToDashboard: () => navigate('/dashboard'), | ||
| goHome: () => navigate('/dashboard'), | ||
| openSatelliteManagement: () => navigate('/dashboard/satellites'), | ||
| openCollisionPrediction: () => navigate('/dashboard/collision-center'), | ||
| openAiAgentDashboard: () => navigate('/dashboard/ai-agents'), | ||
| openManeuverPlanning: () => navigate('/dashboard/mission-planner'), | ||
|
|
||
| // Search | ||
| openGlobalSearch: () => setGlobalSearchOpen(true), | ||
|
|
||
| // Data actions — generic refresh via React Query's cache. | ||
| refreshCurrentData: () => queryClient.invalidateQueries(), | ||
| forceRefreshAllData: () => | ||
| queryClient.invalidateQueries({ refetchType: 'all' }), | ||
|
|
||
| // Dashboard controls | ||
| toggleSidebar: () => toggleSidebar(), | ||
| }); | ||
|
|
||
| return <ShortcutHelpModal isOpen={isHelpOpen} onClose={closeHelp} isMac={isMac} />; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Global shortcuts fire on marketing/auth pages too, not just the dashboard.
<GlobalShortcuts /> is mounted above <Routes>, so all single-letter navigation shortcuts (g/d/s/c/a/h/etc.) are live on /, /about, /docs, /signin, /signup, etc. A visitor reading the landing page or about to sign in can press a plain letter key and get redirected into /dashboard/... unexpectedly, since isTypingContext only guards actively-focused inputs, not "no input focused yet."
Proposed fix: scope shortcuts to dashboard routes via the existing `disabled` option
function GlobalShortcuts() {
const navigate = useNavigate();
const queryClient = useQueryClient();
+ const location = useLocation();
const { toggleSidebar, setGlobalSearchOpen } = useUIStore();
- const { isHelpOpen, closeHelp, isMac } = useKeyboardShortcuts({
+ const { isHelpOpen, closeHelp, isMac } = useKeyboardShortcuts({
openGlobeView: () => navigate('/dashboard'),
...
toggleSidebar: () => toggleSidebar(),
- });
+ }, { disabled: !location.pathname.startsWith('/dashboard') });(useLocation needs importing from react-router-dom alongside the existing useNavigate import.)
Also applies to: 74-74
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/App.tsx` around lines 34 - 64, Scope GlobalShortcuts to
dashboard routes by importing and using useLocation alongside useNavigate, then
pass the existing disabled option to useKeyboardShortcuts when the current
pathname is outside /dashboard (including marketing and authentication pages).
Preserve all shortcut handlers for dashboard routes.
| const { isHelpOpen, closeHelp, isMac } = useKeyboardShortcuts({ | ||
| // Navigation — routes that exist today. Telemetry / Space Weather / | ||
| // Risk Assessment / Prediction Analytics have no page yet, so those | ||
| // shortcuts are left unwired (safe no-ops) until those routes land. | ||
| openGlobeView: () => navigate('/dashboard'), | ||
| goToDashboard: () => navigate('/dashboard'), | ||
| goHome: () => navigate('/dashboard'), | ||
| openSatelliteManagement: () => navigate('/dashboard/satellites'), | ||
| openCollisionPrediction: () => navigate('/dashboard/collision-center'), | ||
| openAiAgentDashboard: () => navigate('/dashboard/ai-agents'), | ||
| openManeuverPlanning: () => navigate('/dashboard/mission-planner'), | ||
|
|
||
| // Search | ||
| openGlobalSearch: () => setGlobalSearchOpen(true), | ||
|
|
||
| // Data actions — generic refresh via React Query's cache. | ||
| refreshCurrentData: () => queryClient.invalidateQueries(), | ||
| forceRefreshAllData: () => | ||
| queryClient.invalidateQueries({ refetchType: 'all' }), | ||
|
|
||
| // Dashboard controls | ||
| toggleSidebar: () => toggleSidebar(), | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
openSettings isn't wired despite the Settings route existing.
Unlike Telemetry/Space Weather/Risk/Prediction (explicitly documented as unwired because those pages don't exist yet), /dashboard/settings already exists (line 97) but util-settings (Ctrl+Shift+S, handlerKey: 'openSettings') has no handler here, so the shortcut is a silent no-op.
Proposed fix
// Dashboard controls
toggleSidebar: () => toggleSidebar(),
+
+ // Utility
+ openSettings: () => navigate('/dashboard/settings'),
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { isHelpOpen, closeHelp, isMac } = useKeyboardShortcuts({ | |
| // Navigation — routes that exist today. Telemetry / Space Weather / | |
| // Risk Assessment / Prediction Analytics have no page yet, so those | |
| // shortcuts are left unwired (safe no-ops) until those routes land. | |
| openGlobeView: () => navigate('/dashboard'), | |
| goToDashboard: () => navigate('/dashboard'), | |
| goHome: () => navigate('/dashboard'), | |
| openSatelliteManagement: () => navigate('/dashboard/satellites'), | |
| openCollisionPrediction: () => navigate('/dashboard/collision-center'), | |
| openAiAgentDashboard: () => navigate('/dashboard/ai-agents'), | |
| openManeuverPlanning: () => navigate('/dashboard/mission-planner'), | |
| // Search | |
| openGlobalSearch: () => setGlobalSearchOpen(true), | |
| // Data actions — generic refresh via React Query's cache. | |
| refreshCurrentData: () => queryClient.invalidateQueries(), | |
| forceRefreshAllData: () => | |
| queryClient.invalidateQueries({ refetchType: 'all' }), | |
| // Dashboard controls | |
| toggleSidebar: () => toggleSidebar(), | |
| }); | |
| const { isHelpOpen, closeHelp, isMac } = useKeyboardShortcuts({ | |
| // Navigation — routes that exist today. Telemetry / Space Weather / | |
| // Risk Assessment / Prediction Analytics have no page yet, so those | |
| // shortcuts are left unwired (safe no-ops) until those routes land. | |
| openGlobeView: () => navigate('/dashboard'), | |
| goToDashboard: () => navigate('/dashboard'), | |
| goHome: () => navigate('/dashboard'), | |
| openSatelliteManagement: () => navigate('/dashboard/satellites'), | |
| openCollisionPrediction: () => navigate('/dashboard/collision-center'), | |
| openAiAgentDashboard: () => navigate('/dashboard/ai-agents'), | |
| openManeuverPlanning: () => navigate('/dashboard/mission-planner'), | |
| // Search | |
| openGlobalSearch: () => setGlobalSearchOpen(true), | |
| // Data actions — generic refresh via React Query's cache. | |
| refreshCurrentData: () => queryClient.invalidateQueries(), | |
| forceRefreshAllData: () => | |
| queryClient.invalidateQueries({ refetchType: 'all' }), | |
| // Dashboard controls | |
| toggleSidebar: () => toggleSidebar(), | |
| // Utility | |
| openSettings: () => navigate('/dashboard/settings'), | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/App.tsx` around lines 39 - 61, Wire the existing openSettings
shortcut in the useKeyboardShortcuts configuration by navigating to the
/dashboard/settings route, alongside the other navigation handlers. Keep the
documented no-op scope limited to routes that do not yet exist.
| // Dashboard Controls — sidebar toggle wired to the existing uiStore. | ||
| { id: 'dash-toggle-sidebar', combo: { key: 'd', modifiers: ['ctrl'] }, description: 'Toggle sidebar', category: 'Dashboard Controls', handlerKey: 'toggleSidebar' }, | ||
| { id: 'dash-collapse-sidebar', combo: { key: 'b', modifiers: ['ctrl'] }, description: 'Collapse or expand sidebar', category: 'Dashboard Controls', handlerKey: 'toggleSidebar' }, | ||
| { id: 'dash-toggle-fullscreen', combo: { key: 'd', modifiers: ['ctrl', 'shift'] }, description: 'Toggle fullscreen dashboard', category: 'Dashboard Controls', handlerKey: 'toggleFullscreenDashboard' }, | ||
| { id: 'dash-toggle-theme', combo: { key: 't', modifiers: ['ctrl'] }, description: 'Toggle theme', category: 'Dashboard Controls', handlerKey: 'toggleTheme' }, | ||
| { id: 'dash-toggle-live', combo: { key: 'l', modifiers: ['ctrl'] }, description: 'Toggle live updates', category: 'Dashboard Controls', handlerKey: 'toggleLiveUpdates' }, | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Map the file and inspect the relevant sections with line numbers.
wc -l frontend/src/utils/keyboardShortcuts.ts
sed -n '1,220p' frontend/src/utils/keyboardShortcuts.ts | cat -n
# Find where the dashboard shortcuts are referenced elsewhere.
rg -n "dash-toggle-theme|toggleTheme|dash-toggle-sidebar|dash-toggle-fullscreen|dash-toggle-live|dash-collapse-sidebar" frontendRepository: 7-Blocks/Kepler
Length of output: 12760
Avoid browser-reserved shortcuts for theme toggle. Ctrl+T is intercepted by the browser for “new tab”, and Ctrl+Shift+T is also reserved in most browsers. Use a non-reserved combo here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/utils/keyboardShortcuts.ts` around lines 118 - 124, Update the
dash-toggle-theme shortcut in the keyboard shortcut definitions to use a
non-browser-reserved key combination instead of Ctrl+T, avoiding both Ctrl+T and
Ctrl+Shift+T while preserving its toggleTheme handler and dashboard-controls
metadata.
User description
Description
Implements a centralized global keyboard shortcut system for Kepler, per issue #83.
utils/keyboardShortcuts.ts— single source of truth for every shortcut in the app: navigation, search, data actions, table navigation, globe controls, dashboard controls, and utility shortcuts. Adding a new shortcut means adding one entry here.hooks/useKeyboardShortcuts.ts— onewindowkeydown listener (no scattered per-component handlers). Ignores typing contexts (inputs, textareas,contenteditable, and rich editors like Monaco/CodeMirror/ProseMirror), resolvesCtrl/Shift/Altcombos (mappingCmd→Ctrlon macOS), and manages the help modal's open/close state internally so?andCtrl+/work with zero extra wiring.components/ShortcutHelpModal.tsx— categorized cheat sheet styled with Kepler's existing design tokens (bg-surface-container,border-border-panel,font-technical-data,MaterialIcon), responsive, closes onEscor backdrop click.App.tsx— wires it all together: navigation shortcuts route to the real dashboard pages,Ctrl+Dtoggles the sidebar via the existinguiStore, andCtrl+R/Ctrl+Shift+Rinvalidate React Query's cache for a generic refresh.Known gap: Telemetry, Space Weather, Risk Assessment, and Prediction Analytics don't have dedicated pages yet, so those four navigation shortcuts are defined and visible in the help modal (per the issue's spec) but intentionally left unwired — pressing them is a safe no-op rather than a 404. They'll just need a handler added in
App.tsxonce those routes exist.Related Issue
Closes #83
Checklist
Screenshots / Screen Recordings
Testing Notes
Manually verified:
?/Ctrl+/open the help modal;Escand backdrop click close itS,C,A,Mnavigate to Satellites, Collision Center, AI Agents, and Mission Planner respectivelyCtrl+Dcollapses/expands the sidebarpnpm buildpasses clean on these changes (two pre-existing, unrelated type errors inProductPage.tsx/DevelopersPage.tsxremain onmainand aren't touched by this PR)Breaking Changes
None.
CodeAnt-AI Description
Add global keyboard shortcuts and an in-app shortcut guide
What Changed
Impact
✅ Faster navigation across the dashboard✅ Quicker access to search and refresh✅ Fewer accidental shortcut triggers while typing💡 Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.
Summary by CodeRabbit
Greptile Summary
This PR adds a centralized global keyboard shortcut system. The main changes are:
Confidence Score: 4/5
Several advertised shortcuts are broken, and dashboard navigation keys run on public and authentication routes.
frontend/src/App.tsx, frontend/src/utils/keyboardShortcuts.ts, frontend/src/components/ShortcutHelpModal.tsx
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD K[Window keydown] --> T{Typing context?} T -->|Yes| I[Ignore shortcut] T -->|No| M[Match key and modifiers] M --> H{Help action?} H -->|Yes| D[Toggle help dialog] H -->|No| R{Handler registered?} R -->|Yes| A[Run application action] R -->|No| N[Silent no-op]Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "feat: add global keyboard shortcut syste..." | Re-trigger Greptile