Skip to content

Commit 987cbb1

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 079c207 commit 987cbb1

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
@@ -23,6 +23,8 @@ import {
2323
HoverCardContent,
2424
} from "../ui/hover-card";
2525
import { version } from "../../../package.json";
26+
import { KeyboardShortcutsModal } from "./keyboard-shortcuts-modal";
27+
import { matchesKey } from "@/browser/lib/shortcuts";
2628

2729
// ============================================================================
2830
// App Shell - Tab-based Layout
@@ -40,6 +42,7 @@ export function AppShell() {
4042
} = useTabContext();
4143
const params = useParams<{ owner: string; repo: string; number: string }>();
4244
const navigate = useNavigate();
45+
const [shortcutsModalOpen, setShortcutsModalOpen] = useState(false);
4346

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

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

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)