From fad5634b29af5d1627cb6449519176e01eac71bf Mon Sep 17 00:00:00 2001 From: dotta Date: Wed, 8 Apr 2026 08:27:49 -0500 Subject: [PATCH] feat(ui): add keyboard shortcut cheatsheet dialog on ? keypress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shows a beautiful categorized cheatsheet of all keyboard shortcuts (inbox, issue detail, global) when the user presses ? with keyboard shortcuts enabled. Respects text input focus detection — won't trigger in text fields. Uses the existing Dialog component and Radix UI. Co-Authored-By: Paperclip --- .../KeyboardShortcutsCheatsheet.tsx | 100 ++++++++++++++++++ ui/src/components/Layout.tsx | 4 + ui/src/hooks/useKeyboardShortcuts.ts | 11 +- 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 ui/src/components/KeyboardShortcutsCheatsheet.tsx diff --git a/ui/src/components/KeyboardShortcutsCheatsheet.tsx b/ui/src/components/KeyboardShortcutsCheatsheet.tsx new file mode 100644 index 00000000..9df4ef7f --- /dev/null +++ b/ui/src/components/KeyboardShortcutsCheatsheet.tsx @@ -0,0 +1,100 @@ +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; + +interface ShortcutEntry { + keys: string[]; + label: string; +} + +interface ShortcutSection { + title: string; + shortcuts: ShortcutEntry[]; +} + +const sections: ShortcutSection[] = [ + { + title: "Inbox", + shortcuts: [ + { keys: ["j"], label: "Move down" }, + { keys: ["k"], label: "Move up" }, + { keys: ["Enter"], label: "Open selected item" }, + { keys: ["a"], label: "Archive item" }, + { keys: ["y"], label: "Archive item" }, + { keys: ["r"], label: "Mark as read" }, + { keys: ["U"], label: "Mark as unread" }, + ], + }, + { + title: "Issue detail", + shortcuts: [ + { keys: ["y"], label: "Quick-archive back to inbox" }, + { keys: ["g", "i"], label: "Go to inbox" }, + ], + }, + { + title: "Global", + shortcuts: [ + { keys: ["c"], label: "New issue" }, + { keys: ["["], label: "Toggle sidebar" }, + { keys: ["]"], label: "Toggle panel" }, + { keys: ["?"], label: "Show keyboard shortcuts" }, + ], + }, +]; + +function KeyCap({ children }: { children: string }) { + return ( + + {children} + + ); +} + +export function KeyboardShortcutsCheatsheet({ + open, + onOpenChange, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + return ( + + + + Keyboard shortcuts + +
+ {sections.map((section) => ( +
+

+ {section.title} +

+
+ {section.shortcuts.map((shortcut) => ( +
+ {shortcut.label} +
+ {shortcut.keys.map((key, i) => ( + + {i > 0 && then} + {key} + + ))} +
+
+ ))} +
+
+ ))} +
+
+

+ Press Esc to close · Shortcuts are disabled in text fields +

+
+
+
+ ); +} diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index be39adb7..9028d7af 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -12,6 +12,7 @@ import { NewIssueDialog } from "./NewIssueDialog"; import { NewProjectDialog } from "./NewProjectDialog"; import { NewGoalDialog } from "./NewGoalDialog"; import { NewAgentDialog } from "./NewAgentDialog"; +import { KeyboardShortcutsCheatsheet } from "./KeyboardShortcutsCheatsheet"; import { ToastViewport } from "./ToastViewport"; import { MobileBottomNav } from "./MobileBottomNav"; import { WorktreeBanner } from "./WorktreeBanner"; @@ -69,6 +70,7 @@ export function Layout() { const lastMainScrollTop = useRef(0); const [mobileNavVisible, setMobileNavVisible] = useState(true); const [instanceSettingsTarget, setInstanceSettingsTarget] = useState(() => readRememberedInstanceSettingsPath()); + const [shortcutsOpen, setShortcutsOpen] = useState(false); const nextTheme = theme === "dark" ? "light" : "dark"; const matchedCompany = useMemo(() => { if (!companyPrefix) return null; @@ -151,6 +153,7 @@ export function Layout() { onNewIssue: () => openNewIssue(), onToggleSidebar: toggleSidebar, onTogglePanel: togglePanel, + onShowShortcuts: () => setShortcutsOpen(true), }); useEffect(() => { @@ -443,6 +446,7 @@ export function Layout() { + diff --git a/ui/src/hooks/useKeyboardShortcuts.ts b/ui/src/hooks/useKeyboardShortcuts.ts index 6f5b33c4..c917ebba 100644 --- a/ui/src/hooks/useKeyboardShortcuts.ts +++ b/ui/src/hooks/useKeyboardShortcuts.ts @@ -6,6 +6,7 @@ interface ShortcutHandlers { onNewIssue?: () => void; onToggleSidebar?: () => void; onTogglePanel?: () => void; + onShowShortcuts?: () => void; } export function useKeyboardShortcuts({ @@ -13,6 +14,7 @@ export function useKeyboardShortcuts({ onNewIssue, onToggleSidebar, onTogglePanel, + onShowShortcuts, }: ShortcutHandlers) { useEffect(() => { if (!enabled) return; @@ -23,6 +25,13 @@ export function useKeyboardShortcuts({ return; } + // ? → Show keyboard shortcuts cheatsheet + if (e.key === "?" && !e.metaKey && !e.ctrlKey && !e.altKey) { + e.preventDefault(); + onShowShortcuts?.(); + return; + } + // C → New Issue if (e.key === "c" && !e.metaKey && !e.ctrlKey && !e.altKey) { e.preventDefault(); @@ -44,5 +53,5 @@ export function useKeyboardShortcuts({ document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); - }, [enabled, onNewIssue, onToggleSidebar, onTogglePanel]); + }, [enabled, onNewIssue, onToggleSidebar, onTogglePanel, onShowShortcuts]); }