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

View File

@@ -5,6 +5,7 @@ import { useCallback, useEffect, useLayoutEffect, useState } from "react";
import { Toaster } from "sonner";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { CommandPalette } from "@/components/workspace/command-palette";
import { WorkspaceSidebar } from "@/components/workspace/workspace-sidebar";
import { getLocalSettings, useLocalSettings } from "@/core/settings";
@@ -39,6 +40,7 @@ export default function WorkspaceLayout({
<WorkspaceSidebar />
<SidebarInset className="min-w-0">{children}</SidebarInset>
</SidebarProvider>
<CommandPalette />
<Toaster position="top-center" />
</QueryClientProvider>
);

View File

@@ -0,0 +1,125 @@
"use client";
import {
KeyboardIcon,
MessageSquarePlusIcon,
SettingsIcon,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useCallback, useMemo, useState } from "react";
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandShortcut,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useI18n } from "@/core/i18n/hooks";
import { useGlobalShortcuts } from "@/hooks/use-global-shortcuts";
export function CommandPalette() {
const { t } = useI18n();
const router = useRouter();
const [open, setOpen] = useState(false);
const [shortcutsOpen, setShortcutsOpen] = useState(false);
const handleNewChat = useCallback(() => {
router.push("/workspace/chats/new");
setOpen(false);
}, [router]);
const handleOpenSettings = useCallback(() => {
router.push("/workspace/settings");
setOpen(false);
}, [router]);
const handleShowShortcuts = useCallback(() => {
setOpen(false);
setShortcutsOpen(true);
}, []);
const shortcuts = useMemo(
() => [
{ key: "k", meta: true, action: () => setOpen((o) => !o) },
{ key: "n", meta: true, shift: true, action: handleNewChat },
{ key: ",", meta: true, action: handleOpenSettings },
{ key: "/", meta: true, action: handleShowShortcuts },
],
[handleNewChat, handleOpenSettings, handleShowShortcuts],
);
useGlobalShortcuts(shortcuts);
const isMac =
typeof navigator !== "undefined" && navigator.userAgent.includes("Mac");
const metaKey = isMac ? "⌘" : "Ctrl+";
const shiftKey = isMac ? "⇧" : "Shift+";
return (
<>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder={t.shortcuts.searchActions} />
<CommandList>
<CommandEmpty>{t.shortcuts.noResults}</CommandEmpty>
<CommandGroup heading={t.shortcuts.actions}>
<CommandItem onSelect={handleNewChat}>
<MessageSquarePlusIcon className="mr-2 h-4 w-4" />
{t.sidebar.newChat}
<CommandShortcut>{metaKey}{shiftKey}N</CommandShortcut>
</CommandItem>
<CommandItem onSelect={handleOpenSettings}>
<SettingsIcon className="mr-2 h-4 w-4" />
{t.common.settings}
<CommandShortcut>{metaKey},</CommandShortcut>
</CommandItem>
<CommandItem onSelect={handleShowShortcuts}>
<KeyboardIcon className="mr-2 h-4 w-4" />
{t.shortcuts.keyboardShortcuts}
<CommandShortcut>{metaKey}/</CommandShortcut>
</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
<Dialog open={shortcutsOpen} onOpenChange={setShortcutsOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t.shortcuts.keyboardShortcuts}</DialogTitle>
<DialogDescription>
{t.shortcuts.keyboardShortcutsDescription}
</DialogDescription>
</DialogHeader>
<div className="space-y-3 text-sm">
{[
{ keys: `${metaKey}K`, label: t.shortcuts.openCommandPalette },
{ keys: `${metaKey}${shiftKey}N`, label: t.sidebar.newChat },
{ keys: `${metaKey}B`, label: t.shortcuts.toggleSidebar },
{ keys: `${metaKey},`, label: t.common.settings },
{
keys: `${metaKey}/`,
label: t.shortcuts.keyboardShortcuts,
},
].map(({ keys, label }) => (
<div key={keys} className="flex items-center justify-between">
<span className="text-muted-foreground">{label}</span>
<kbd className="bg-muted text-muted-foreground rounded px-2 py-0.5 font-mono text-xs">
{keys}
</kbd>
</div>
))}
</div>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -274,6 +274,17 @@ export const enUS: Translations = {
failed: "Subtask failed",
},
// Shortcuts
shortcuts: {
searchActions: "Search actions...",
noResults: "No results found.",
actions: "Actions",
keyboardShortcuts: "Keyboard Shortcuts",
keyboardShortcutsDescription: "Navigate DeerFlow faster with keyboard shortcuts.",
openCommandPalette: "Open Command Palette",
toggleSidebar: "Toggle Sidebar",
},
// Settings
settings: {
title: "Settings",

View File

@@ -211,6 +211,17 @@ export interface Translations {
failed: string;
};
// Shortcuts
shortcuts: {
searchActions: string;
noResults: string;
actions: string;
keyboardShortcuts: string;
keyboardShortcutsDescription: string;
openCommandPalette: string;
toggleSidebar: string;
};
// Settings
settings: {
title: string;

View File

@@ -261,6 +261,17 @@ export const zhCN: Translations = {
failed: "子任务失败",
},
// Shortcuts
shortcuts: {
searchActions: "搜索操作...",
noResults: "未找到结果。",
actions: "操作",
keyboardShortcuts: "键盘快捷键",
keyboardShortcutsDescription: "使用键盘快捷键更快地操作 DeerFlow。",
openCommandPalette: "打开命令面板",
toggleSidebar: "切换侧边栏",
},
// Settings
settings: {
title: "设置",

View File

@@ -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]);
}