diff --git a/packages/app/e2e/keyboard-shortcuts-dialog.spec.ts b/packages/app/e2e/keyboard-shortcuts-dialog.spec.ts
new file mode 100644
index 000000000..d09b9f5f3
--- /dev/null
+++ b/packages/app/e2e/keyboard-shortcuts-dialog.spec.ts
@@ -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");
+});
diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx
index 75bcc058f..dac040cba 100644
--- a/packages/app/src/app/_layout.tsx
+++ b/packages/app/src/app/_layout.tsx
@@ -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 && }
+
);
diff --git a/packages/app/src/components/keyboard-shortcuts-dialog.tsx b/packages/app/src/components/keyboard-shortcuts-dialog.tsx
new file mode 100644
index 000000000..f68065323
--- /dev/null
+++ b/packages/app/src/components/keyboard-shortcuts-dialog.tsx
@@ -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 (
+ setOpen(false)}
+ testID="keyboard-shortcuts-dialog"
+ snapPoints={["70%", "92%"]}
+ >
+
+ {sections.map((section) => (
+
+ {section.title}
+
+ {section.rows.map((row) => (
+
+
+ {row.label}
+ {row.note ? {row.note} : null}
+
+
+
+ ))}
+
+
+ ))}
+
+
+ );
+}
+
+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",
+ },
+}));
diff --git a/packages/app/src/hooks/use-keyboard-shortcuts.ts b/packages/app/src/hooks/use-keyboard-shortcuts.ts
index 1fb6ee500..50efb0a29 100644
--- a/packages/app/src/hooks/use-keyboard-shortcuts.ts
+++ b/packages/app/src/hooks/use-keyboard-shortcuts.ts
@@ -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;
diff --git a/packages/app/src/keyboard/actions.ts b/packages/app/src/keyboard/actions.ts
index 03e3dfe8e..a626d5c60 100644
--- a/packages/app/src/keyboard/actions.ts
+++ b/packages/app/src/keyboard/actions.ts
@@ -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 =
diff --git a/packages/app/src/keyboard/keyboard-shortcuts.test.ts b/packages/app/src/keyboard/keyboard-shortcuts.test.ts
index aabc6f66c..fe14d5d47 100644
--- a/packages/app/src/keyboard/keyboard-shortcuts.test.ts
+++ b/packages/app/src/keyboard/keyboard-shortcuts.test.ts
@@ -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({
diff --git a/packages/app/src/keyboard/keyboard-shortcuts.ts b/packages/app/src/keyboard/keyboard-shortcuts.ts
index e80cc233c..2e63e657a 100644
--- a/packages/app/src/keyboard/keyboard-shortcuts.ts
+++ b/packages/app/src/keyboard/keyboard-shortcuts.ts
@@ -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",
diff --git a/packages/app/src/stores/keyboard-shortcuts-store.ts b/packages/app/src/stores/keyboard-shortcuts-store.ts
index 1452a0b06..dd0609438 100644
--- a/packages/app/src/stores/keyboard-shortcuts-store.ts
+++ b/packages/app/src/stores/keyboard-shortcuts-store.ts
@@ -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(
(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 }),