feat(app): add global shortcut help dialog

This commit is contained in:
Mohamed Boudra
2026-02-14 20:18:28 +07:00
parent 756f9a7972
commit 9da5b26db0
8 changed files with 278 additions and 1 deletions
@@ -0,0 +1,17 @@
import { test, expect } from "./fixtures";
import { gotoHome } from "./helpers/app";
test("question mark opens keyboard shortcuts dialog", async ({ page }) => {
await gotoHome(page);
await page.getByTestId("menu-button").first().focus();
await page.keyboard.press("Shift+/");
const dialog = page.getByTestId("keyboard-shortcuts-dialog");
const content = page.getByTestId("keyboard-shortcuts-dialog-content");
await expect(dialog).toBeVisible({ timeout: 10000 });
await expect(content).toBeVisible({ timeout: 10000 });
await expect(content).toContainText("Show keyboard shortcuts");
await expect(content).toContainText("Toggle left sidebar");
});
+2
View File
@@ -36,6 +36,7 @@ import {
import { getIsTauri, getIsTauriMac } from "@/constants/layout";
import { useTrafficLightPadding } from "@/utils/tauri-window";
import { CommandCenter } from "@/components/command-center";
import { KeyboardShortcutsDialog } from "@/components/keyboard-shortcuts-dialog";
import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
import { queryClient } from "@/query/query-client";
import {
@@ -255,6 +256,7 @@ function AppContainer({ children, selectedAgentId }: AppContainerProps) {
{isMobile && chromeEnabled && <SlidingSidebar selectedAgentId={selectedAgentId} />}
<DownloadToast />
<CommandCenter />
<KeyboardShortcutsDialog />
</View>
);
@@ -0,0 +1,180 @@
import { useMemo } from "react";
import { Text, View } from "react-native";
import { StyleSheet } from "react-native-unistyles";
import { getIsTauri } from "@/constants/layout";
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
import { Shortcut } from "@/components/ui/shortcut";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import type { ShortcutKey } from "@/utils/format-shortcut";
import { getShortcutOs } from "@/utils/shortcut-platform";
type ShortcutHelpRow = {
id: string;
label: string;
keys: ShortcutKey[];
note?: string;
};
type ShortcutHelpSection = {
title: string;
rows: ShortcutHelpRow[];
};
function buildShortcutHelpSections(input: {
isMac: boolean;
isTauri: boolean;
}): ShortcutHelpSection[] {
const leftSidebarKeys: ShortcutKey[] = input.isMac ? ["mod", "B"] : ["mod", "."];
const newAgentKeys: ShortcutKey[] = input.isTauri
? ["mod", "N"]
: ["mod", "alt", "N"];
const quickAgentKeys: ShortcutKey[] = input.isTauri
? ["mod", "1-9"]
: ["alt", "1-9"];
return [
{
title: "Global",
rows: [
{
id: "show-shortcuts",
label: "Show keyboard shortcuts",
keys: ["?"],
note: "Available when focus is not in a text field or terminal.",
},
{
id: "toggle-left-sidebar",
label: "Toggle left sidebar",
keys: leftSidebarKeys,
},
{
id: "toggle-right-sidebar",
label: "Toggle right sidebar",
keys: ["mod", "E"],
},
{
id: "toggle-command-center",
label: "Toggle command center",
keys: ["mod", "K"],
},
{
id: "new-agent",
label: "Create new agent",
keys: newAgentKeys,
},
{
id: "quick-open-agent",
label: "Open sidebar agent shortcut",
keys: quickAgentKeys,
},
],
},
{
title: "Agent Input",
rows: [
{
id: "dictation-toggle",
label: "Start/stop dictation",
keys: ["mod", "D"],
},
{
id: "voice-toggle",
label: "Toggle voice mode",
keys: ["mod", "shift", "D"],
},
{
id: "voice-mute-toggle",
label: "Mute/unmute voice mode",
keys: ["Space"],
},
],
},
];
}
export function KeyboardShortcutsDialog() {
const open = useKeyboardShortcutsStore((s) => s.shortcutsDialogOpen);
const setOpen = useKeyboardShortcutsStore((s) => s.setShortcutsDialogOpen);
const isMac = getShortcutOs() === "mac";
const isTauri = getIsTauri();
const sections = useMemo(
() => buildShortcutHelpSections({ isMac, isTauri }),
[isMac, isTauri]
);
return (
<AdaptiveModalSheet
title="Keyboard shortcuts"
visible={open}
onClose={() => setOpen(false)}
testID="keyboard-shortcuts-dialog"
snapPoints={["70%", "92%"]}
>
<View testID="keyboard-shortcuts-dialog-content" style={styles.content}>
{sections.map((section) => (
<View key={section.title} style={styles.section}>
<Text style={styles.sectionTitle}>{section.title}</Text>
<View style={styles.rows}>
{section.rows.map((row) => (
<View key={row.id} style={styles.row}>
<View style={styles.rowText}>
<Text style={styles.rowLabel}>{row.label}</Text>
{row.note ? <Text style={styles.rowNote}>{row.note}</Text> : null}
</View>
<Shortcut keys={row.keys} style={styles.rowShortcut} />
</View>
))}
</View>
</View>
))}
</View>
</AdaptiveModalSheet>
);
}
const styles = StyleSheet.create((theme) => ({
content: {
gap: theme.spacing[4],
},
section: {
gap: theme.spacing[2],
},
sectionTitle: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.medium,
color: theme.colors.foregroundMuted,
},
rows: {
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.surface2,
borderRadius: theme.borderRadius.lg,
overflow: "hidden",
},
row: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: theme.spacing[3],
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[3],
borderBottomWidth: theme.borderWidth[1],
borderBottomColor: theme.colors.surface2,
},
rowText: {
flex: 1,
minWidth: 0,
},
rowLabel: {
fontSize: theme.fontSize.sm,
color: theme.colors.foreground,
},
rowNote: {
marginTop: 2,
fontSize: theme.fontSize.xs,
color: theme.colors.foregroundMuted,
},
rowShortcut: {
alignSelf: "flex-start",
},
}));
@@ -179,6 +179,11 @@ export function useKeyboardShortcuts({
store.setCommandCenterOpen(!store.commandCenterOpen);
return true;
}
case "shortcuts.dialog.toggle": {
const store = useKeyboardShortcutsStore.getState();
store.setShortcutsDialogOpen(!store.shortcutsDialogOpen);
return true;
}
case "message-input.action":
if (!input.payload || typeof input.payload !== "object" || !("kind" in input.payload)) {
return false;
+1
View File
@@ -18,6 +18,7 @@ export type KeyboardActionId =
| "sidebar.toggle.right"
| "sidebar.navigate.shortcut"
| "command-center.toggle"
| "shortcuts.dialog.toggle"
| "message-input.action";
export type KeyboardShortcutPayload =
@@ -31,6 +31,32 @@ function shortcutContext(
}
describe("keyboard-shortcuts", () => {
it("matches question-mark shortcut to toggle the shortcuts dialog", () => {
const match = resolveKeyboardShortcut({
event: keyboardEvent({
key: "?",
code: "Slash",
shiftKey: true,
}),
context: shortcutContext({ focusScope: "other" }),
});
expect(match?.action).toBe("shortcuts.dialog.toggle");
});
it("does not match question-mark shortcut inside editable scopes", () => {
const match = resolveKeyboardShortcut({
event: keyboardEvent({
key: "?",
code: "Slash",
shiftKey: true,
}),
context: shortcutContext({ focusScope: "message-input" }),
});
expect(match).toBeNull();
});
it("matches Cmd+B sidebar toggle on macOS", () => {
const match = resolveKeyboardShortcut({
event: keyboardEvent({
@@ -97,6 +123,31 @@ describe("keyboard-shortcuts", () => {
expect(match).toBeNull();
});
it("keeps space typing available in message input", () => {
const match = resolveKeyboardShortcut({
event: keyboardEvent({
key: " ",
code: "Space",
}),
context: shortcutContext({ focusScope: "message-input" }),
});
expect(match).toBeNull();
});
it("routes space to voice mute toggle outside editable scopes", () => {
const match = resolveKeyboardShortcut({
event: keyboardEvent({
key: " ",
code: "Space",
}),
context: shortcutContext({ focusScope: "other" }),
});
expect(match?.action).toBe("message-input.action");
expect(match?.payload).toEqual({ kind: "voice-mute-toggle" });
});
it("parses Alt+digit sidebar shortcut payload", () => {
const match = resolveKeyboardShortcut({
event: keyboardEvent({
@@ -55,6 +55,17 @@ function hasDigit(event: KeyboardEvent): boolean {
return parseDigit(event) !== null;
}
function isQuestionMarkShortcut(event: KeyboardEvent): boolean {
return (
!event.metaKey &&
!event.ctrlKey &&
!event.altKey &&
event.shiftKey &&
!event.repeat &&
(event.key === "?" || event.code === "Slash")
);
}
function withMessageInputAction(
kind: MessageInputKeyboardActionKind
): (event: KeyboardEvent) => KeyboardShortcutPayload {
@@ -92,6 +103,12 @@ const SHORTCUT_BINDINGS: readonly KeyboardShortcutBinding[] = [
(event.code === "KeyK" || event.key.toLowerCase() === "k"),
when: () => true,
},
{
id: "shortcuts-dialog-toggle-question-mark",
action: "shortcuts.dialog.toggle",
matches: isQuestionMarkShortcut,
when: (context) => context.focusScope === "other",
},
{
id: "sidebar-toggle-left-mac-cmd-b",
action: "sidebar.toggle.left",
@@ -179,7 +196,7 @@ const SHORTCUT_BINDINGS: readonly KeyboardShortcutBinding[] = [
!event.repeat,
payload: withMessageInputAction("voice-mute-toggle"),
when: (context) =>
!context.commandCenterOpen && context.focusScope !== "terminal",
!context.commandCenterOpen && context.focusScope === "other",
},
{
id: "sidebar-shortcut-alt-digit",
@@ -9,6 +9,7 @@ export type MessageInputActionRequest = {
interface KeyboardShortcutsState {
commandCenterOpen: boolean;
shortcutsDialogOpen: boolean;
altDown: boolean;
cmdOrCtrlDown: boolean;
/** Sidebar-visible agent keys (up to 9), in top-to-bottom visual order. */
@@ -16,6 +17,7 @@ interface KeyboardShortcutsState {
messageInputActionRequest: MessageInputActionRequest | null;
setCommandCenterOpen: (open: boolean) => void;
setShortcutsDialogOpen: (open: boolean) => void;
setAltDown: (down: boolean) => void;
setCmdOrCtrlDown: (down: boolean) => void;
setSidebarShortcutAgentKeys: (keys: string[]) => void;
@@ -31,12 +33,14 @@ interface KeyboardShortcutsState {
export const useKeyboardShortcutsStore = create<KeyboardShortcutsState>(
(set, get) => ({
commandCenterOpen: false,
shortcutsDialogOpen: false,
altDown: false,
cmdOrCtrlDown: false,
sidebarShortcutAgentKeys: [],
messageInputActionRequest: null,
setCommandCenterOpen: (open) => set({ commandCenterOpen: open }),
setShortcutsDialogOpen: (open) => set({ shortcutsDialogOpen: open }),
setAltDown: (down) => set({ altDown: down }),
setCmdOrCtrlDown: (down) => set({ cmdOrCtrlDown: down }),
setSidebarShortcutAgentKeys: (keys) => set({ sidebarShortcutAgentKeys: keys }),