Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
456 changes: 456 additions & 0 deletions apps/code/tests/e2e/tests/shortcuts.spec.ts

Large diffs are not rendered by default.

122 changes: 122 additions & 0 deletions packages/ui/src/features/command/keyboard-shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const SHORTCUTS = {
SPACE_UP: "mod+up",
SPACE_DOWN: "mod+down",
FIND_IN_CONVERSATION: "mod+f",
FILE_PICKER: "mod+p",
BLUR: "escape",
SUBMIT_BLUR: "mod+enter",
} as const;
Expand All @@ -35,6 +36,7 @@ export interface KeyboardShortcut {
category: ShortcutCategory;
context?: string;
alternateKeys?: string;
configurable?: boolean;
}

export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [
Expand All @@ -44,30 +46,35 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [
description: "New task",
category: "general",
alternateKeys: "mod+t",
configurable: true,
},
{
id: "command-menu",
keys: SHORTCUTS.COMMAND_MENU,
description: "Open command menu",
category: "general",
configurable: true,
},
{
id: "settings",
keys: SHORTCUTS.SETTINGS,
description: "Open settings",
category: "general",
configurable: true,
},
{
id: "shortcuts",
keys: SHORTCUTS.SHORTCUTS_SHEET,
description: "Show keyboard shortcuts",
category: "general",
configurable: true,
},
{
id: "inbox",
keys: SHORTCUTS.INBOX,
description: "Open inbox",
category: "navigation",
configurable: true,
},
{
id: "switch-task",
Expand All @@ -81,49 +88,57 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [
description: "Previous task",
category: "navigation",
alternateKeys: "ctrl+shift+tab",
configurable: true,
},
{
id: "next-task",
keys: "mod+shift+]",
description: "Next task",
category: "navigation",
alternateKeys: "ctrl+tab",
configurable: true,
},
{
id: "space-up",
keys: SHORTCUTS.SPACE_UP,
description: "Previous space",
category: "navigation",
configurable: true,
},
{
id: "space-down",
keys: SHORTCUTS.SPACE_DOWN,
description: "Next space",
category: "navigation",
configurable: true,
},
{
id: "go-back",
keys: SHORTCUTS.GO_BACK,
description: "Go back",
category: "navigation",
configurable: true,
},
{
id: "go-forward",
keys: SHORTCUTS.GO_FORWARD,
description: "Go forward",
category: "navigation",
configurable: true,
},
{
id: "toggle-left-sidebar",
keys: SHORTCUTS.TOGGLE_LEFT_SIDEBAR,
description: "Toggle left sidebar",
category: "navigation",
configurable: true,
},
{
id: "toggle-review-panel",
keys: SHORTCUTS.TOGGLE_REVIEW_PANEL,
description: "Toggle review panel",
category: "navigation",
configurable: true,
},
{
id: "switch-tab",
Expand All @@ -138,20 +153,31 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [
description: "Close active tab",
category: "panels",
context: "Task detail",
configurable: true,
},
{
id: "open-in-editor",
keys: SHORTCUTS.OPEN_IN_EDITOR,
description: "Open in external editor",
category: "panels",
context: "Task detail",
configurable: true,
},
{
id: "copy-path",
keys: SHORTCUTS.COPY_PATH,
description: "Copy file path",
category: "panels",
context: "Task detail",
configurable: true,
},
{
id: "toggle-focus",
keys: SHORTCUTS.TOGGLE_FOCUS,
description: "Toggle focus mode",
category: "panels",
context: "Task detail (worktree)",
configurable: true,
},
{
id: "find-in-conversation",
Expand All @@ -160,26 +186,37 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [
category: "panels",
context: "Task detail",
},
{
id: "file-picker",
keys: SHORTCUTS.FILE_PICKER,
description: "Open file picker",
category: "panels",
context: "Task detail",
configurable: true,
},
{
id: "paste-as-file",
keys: SHORTCUTS.PASTE_AS_FILE,
description: "Paste as file attachment",
category: "editor",
context: "Message editor",
configurable: true,
},
{
id: "prompt-history-prev",
keys: "up",
description: "Previous prompt (when input is empty)",
category: "editor",
context: "Message editor",
configurable: true,
},
{
id: "prompt-history-next",
keys: "down",
description: "Next prompt (when input is empty)",
category: "editor",
context: "Message editor",
configurable: true,
},
{
id: "editor-bold",
Expand Down Expand Up @@ -218,6 +255,56 @@ export const CATEGORY_LABELS: Record<ShortcutCategory, string> = {
editor: "Editor",
};

export const CONFIGURABLE_SHORTCUT_IDS = [
"command-menu",
"new-task",
"settings",
"shortcuts",
"inbox",
"prev-task",
"next-task",
"space-up",
"space-down",
"go-back",
"go-forward",
"toggle-left-sidebar",
"toggle-review-panel",
"close-tab",
"open-in-editor",
"copy-path",
"toggle-focus",
"file-picker",
"paste-as-file",
"prompt-history-prev",
"prompt-history-next",
] as const;

export type ConfigurableShortcutId = (typeof CONFIGURABLE_SHORTCUT_IDS)[number];

export const DEFAULT_KEYBINDINGS: Record<ConfigurableShortcutId, string> = {
"command-menu": SHORTCUTS.COMMAND_MENU,
"new-task": SHORTCUTS.NEW_TASK,
settings: SHORTCUTS.SETTINGS,
shortcuts: SHORTCUTS.SHORTCUTS_SHEET,
inbox: SHORTCUTS.INBOX,
"prev-task": SHORTCUTS.PREV_TASK,
"next-task": SHORTCUTS.NEXT_TASK,
"space-up": SHORTCUTS.SPACE_UP,
"space-down": SHORTCUTS.SPACE_DOWN,
"go-back": SHORTCUTS.GO_BACK,
"go-forward": SHORTCUTS.GO_FORWARD,
"toggle-left-sidebar": SHORTCUTS.TOGGLE_LEFT_SIDEBAR,
"toggle-review-panel": SHORTCUTS.TOGGLE_REVIEW_PANEL,
"close-tab": SHORTCUTS.CLOSE_TAB,
"open-in-editor": SHORTCUTS.OPEN_IN_EDITOR,
"copy-path": SHORTCUTS.COPY_PATH,
"toggle-focus": SHORTCUTS.TOGGLE_FOCUS,
"file-picker": SHORTCUTS.FILE_PICKER,
"paste-as-file": SHORTCUTS.PASTE_AS_FILE,
"prompt-history-prev": "shift+up",
"prompt-history-next": "shift+down",
};

export function getShortcutsByCategory(): Record<
ShortcutCategory,
KeyboardShortcut[]
Expand All @@ -234,6 +321,41 @@ export function getShortcutsByCategory(): Record<
return grouped;
}

/**
* Convert a DOM KeyboardEvent to the normalised combo string used by the
* keybindings store (e.g. "mod+shift+v"). Returns null for bare modifier presses.
*/
export function eventToCombo(e: KeyboardEvent): string | null {
const bare = ["Meta", "Control", "Shift", "Alt"];
if (bare.includes(e.key)) return null;
if (!(e.metaKey || e.ctrlKey || e.altKey)) return null;

const parts: string[] = [];
if (e.metaKey || e.ctrlKey) parts.push("mod");
if (e.shiftKey) parts.push("shift");
if (e.altKey) parts.push("alt");
// Normalize "ArrowUp" → "up", "ArrowDown" → "down", etc. to match stored bindings.
parts.push(e.key.toLowerCase().replace(/^arrow/, ""));
return parts.join("+");
}

/**
* Like eventToCombo but also accepts shift-only combos (no ctrl/meta/alt required).
* Used inside Tiptap's handleKeyDown to match bindings such as "shift+up".
*/
export function tiptapEventToCombo(e: KeyboardEvent): string | null {
const bare = ["Meta", "Control", "Shift", "Alt"];
if (bare.includes(e.key)) return null;
if (!(e.metaKey || e.ctrlKey || e.altKey || e.shiftKey)) return null;

const parts: string[] = [];
if (e.metaKey || e.ctrlKey) parts.push("mod");
if (e.shiftKey) parts.push("shift");
if (e.altKey) parts.push("alt");
parts.push(e.key.toLowerCase().replace(/^arrow/, ""));
return parts.join("+");
}

function formatKey(key: string): string {
const k = key.trim().toLowerCase();
if (k === "mod") return isMac ? "⌘" : "Ctrl";
Expand Down
Loading