feat(frontend): add Cmd+K command palette and keyboard shortcuts (#1230)

* feat(frontend): add Cmd+K command palette and keyboard shortcuts

Wire up the existing shadcn/ui Command component as a global command
palette. Adds a useGlobalShortcuts hook for Cmd+K (palette), Cmd+Shift+N
(new chat), Cmd+, (settings), and Cmd+/ (shortcuts help overlay).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(frontend): address Copilot review feedback on command palette

- Normalize event.key with toLowerCase() for reliable Shift+key matching
- Replace dead deerflow:open-settings event with router.push navigation
- Use platform-appropriate Shift label (Shift+ on Windows/Linux, glyph on Mac)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Matt Van Horn
2026-03-23 03:35:35 -07:00
committed by GitHub
parent a29134d7c9
commit 48031e506b
6 changed files with 213 additions and 0 deletions
@@ -0,0 +1,53 @@
"use client";
import { useEffect } from "react";
type ShortcutAction = () => void;
interface Shortcut {
key: string;
meta: boolean;
shift?: boolean;
action: ShortcutAction;
}
/**
* Register global keyboard shortcuts on window.
* Shortcuts are suppressed when focus is inside an input, textarea, or
* contentEditable element - except for Cmd+K which always fires.
*/
export function useGlobalShortcuts(shortcuts: Shortcut[]) {
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
const meta = event.metaKey || event.ctrlKey;
for (const shortcut of shortcuts) {
if (
event.key.toLowerCase() === shortcut.key.toLowerCase() &&
meta === shortcut.meta &&
(shortcut.shift ?? false) === event.shiftKey
) {
// Allow Cmd+K even in inputs (standard command palette behavior)
if (shortcut.key !== "k") {
const target = event.target as HTMLElement;
const tag = target.tagName;
if (
tag === "INPUT" ||
tag === "TEXTAREA" ||
target.isContentEditable
) {
continue;
}
}
event.preventDefault();
shortcut.action();
return;
}
}
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [shortcuts]);
}