Skip to content

Commit 76c3f38

Browse files
committed
Add keyboard shortcuts help modal with centralized config
- Add `?` hotkey to open a modal showing all available shortcuts - Create centralized shortcuts config in src/browser/lib/shortcuts.ts - Modal auto-generates from config (no manual sync needed) - Update all keyboard handlers to use matchesKey() utility - Shortcuts organized by category: Navigation, Actions, Go to Line, File Search, Tabs, Help Change-Id: I4c7d8b209ed70901f7b6e3c61c275a34207f7369 Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent 2020b7e commit 76c3f38

File tree

5 files changed

+495
-121
lines changed

5 files changed

+495
-121
lines changed

src/browser/components/app-shell.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import {
2222
HoverCardTrigger,
2323
HoverCardContent,
2424
} from "../ui/hover-card";
25+
import { KeyboardShortcutsModal } from "./keyboard-shortcuts-modal";
26+
import { matchesKey } from "@/browser/lib/shortcuts";
2527

2628
// ============================================================================
2729
// App Shell - Tab-based Layout
@@ -39,6 +41,7 @@ export function AppShell() {
3941
} = useTabContext();
4042
const params = useParams<{ owner: string; repo: string; number: string }>();
4143
const navigate = useNavigate();
44+
const [shortcutsModalOpen, setShortcutsModalOpen] = useState(false);
4245

4346
// URL is the source of truth - sync URL → Tab
4447
useEffect(() => {
@@ -115,6 +118,26 @@ export function AppShell() {
115118
return () => window.removeEventListener("keydown", handleKeyDown);
116119
}, [tabs, activeTabId, handleTabSelect, closeTab]);
117120

121+
// Handle ? key for keyboard shortcuts modal
122+
useEffect(() => {
123+
const handleKeyDown = (e: KeyboardEvent) => {
124+
const target = e.target as HTMLElement;
125+
if (
126+
target.tagName === "INPUT" ||
127+
target.tagName === "TEXTAREA" ||
128+
target.isContentEditable
129+
) {
130+
return;
131+
}
132+
if (matchesKey(e, "SHOW_SHORTCUTS")) {
133+
e.preventDefault();
134+
setShortcutsModalOpen(true);
135+
}
136+
};
137+
window.addEventListener("keydown", handleKeyDown);
138+
return () => window.removeEventListener("keydown", handleKeyDown);
139+
}, []);
140+
118141
return (
119142
<div className="h-screen flex flex-col overflow-hidden bg-background">
120143
{/* Native-style Tab Bar */}
@@ -209,6 +232,11 @@ export function AppShell() {
209232
</div>
210233
)}
211234
</div>
235+
236+
<KeyboardShortcutsModal
237+
open={shortcutsModalOpen}
238+
onOpenChange={setShortcutsModalOpen}
239+
/>
212240
</div>
213241
);
214242
}

src/browser/components/command-palette.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { cn } from "../cn";
1818
import { usePRReviewSelector, usePRReviewStore } from "../contexts/pr-review";
1919
import { Keycap, KeycapGroup } from "../ui/keycap";
2020
import type { PullRequestFile } from "@/api/types";
21+
import { matchesKey } from "@/browser/lib/shortcuts";
2122

2223
// ============================================================================
2324
// Global Command Palette Context
@@ -44,7 +45,10 @@ export function CommandPaletteProvider({ children }: { children: ReactNode }) {
4445
useEffect(() => {
4546
const handleKeyDown = (e: KeyboardEvent) => {
4647
// Ctrl+K or Ctrl+P to open command palette
47-
if ((e.ctrlKey || e.metaKey) && (e.key === "k" || e.key === "p")) {
48+
if (
49+
matchesKey(e, "OPEN_FILE_SEARCH_K") ||
50+
matchesKey(e, "OPEN_FILE_SEARCH_P")
51+
) {
4852
e.preventDefault();
4953
e.stopPropagation();
5054
setOpen((prev) => !prev);
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {
2+
Dialog,
3+
DialogContent,
4+
DialogHeader,
5+
DialogTitle,
6+
DialogDescription,
7+
} from "@/browser/ui/dialog";
8+
import { KeycapGroup } from "@/browser/ui/keycap";
9+
import { getShortcutsByCategory } from "@/browser/lib/shortcuts";
10+
11+
interface KeyboardShortcutsModalProps {
12+
open: boolean;
13+
onOpenChange: (open: boolean) => void;
14+
}
15+
16+
export function KeyboardShortcutsModal({
17+
open,
18+
onOpenChange,
19+
}: KeyboardShortcutsModalProps) {
20+
const categories = getShortcutsByCategory();
21+
22+
return (
23+
<Dialog open={open} onOpenChange={onOpenChange}>
24+
<DialogContent className="sm:max-w-xl max-h-[85vh] overflow-y-auto">
25+
<DialogHeader>
26+
<DialogTitle>Keyboard Shortcuts</DialogTitle>
27+
<DialogDescription className="sr-only">
28+
A list of all available keyboard shortcuts
29+
</DialogDescription>
30+
</DialogHeader>
31+
<div className="grid gap-6 pt-2">
32+
{categories.map(({ category, shortcuts }) => (
33+
<div key={category}>
34+
<h3 className="text-sm font-medium text-muted-foreground mb-2">
35+
{category}
36+
</h3>
37+
<div className="space-y-1.5">
38+
{shortcuts.map((shortcut, idx) => (
39+
<div
40+
key={idx}
41+
className="flex items-center justify-between py-1"
42+
>
43+
<span className="text-sm">{shortcut.description}</span>
44+
<KeycapGroup keys={shortcut.keys} size="sm" />
45+
</div>
46+
))}
47+
</div>
48+
</div>
49+
))}
50+
</div>
51+
</DialogContent>
52+
</Dialog>
53+
);
54+
}

0 commit comments

Comments
 (0)