From 3a4597028072f28f1a202f083131dc0a3ba0a981 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:34:42 +0000 Subject: [PATCH] Add keyboard shortcuts customization UI to settings view Co-authored-by: GTheMachine <156854865+GTheMachine@users.noreply.github.com> --- app/src/components/views/settings-view.tsx | 397 ++++++++++++++++++++- 1 file changed, 396 insertions(+), 1 deletion(-) diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 57f2c839..e399c09f 100644 --- a/app/src/components/views/settings-view.tsx +++ b/app/src/components/views/settings-view.tsx @@ -1,7 +1,8 @@ "use client"; import { useState, useEffect, useRef, useCallback } from "react"; -import { useAppStore } from "@/store/app-store"; +import { useAppStore, DEFAULT_KEYBOARD_SHORTCUTS } from "@/store/app-store"; +import type { KeyboardShortcuts } from "@/store/app-store"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -58,6 +59,7 @@ const NAV_ITEMS = [ { id: "codex", label: "Codex", icon: Atom }, { id: "appearance", label: "Appearance", icon: Palette }, { id: "kanban", label: "Kanban Display", icon: LayoutGrid }, + { id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 }, { id: "defaults", label: "Feature Defaults", icon: FlaskConical }, { id: "danger", label: "Danger Zone", icon: Trash2 }, ]; @@ -79,6 +81,9 @@ export function SettingsView() { setShowProfilesOnly, currentProject, moveProjectToTrash, + keyboardShortcuts, + setKeyboardShortcut, + resetKeyboardShortcuts, } = useAppStore(); const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic); const [googleKey, setGoogleKey] = useState(apiKeys.google); @@ -137,6 +142,9 @@ export function SettingsView() { const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false); const [isCheckingCodexCli, setIsCheckingCodexCli] = useState(false); + const [editingShortcut, setEditingShortcut] = useState(null); + const [shortcutValue, setShortcutValue] = useState(""); + const [shortcutError, setShortcutError] = useState(null); const scrollContainerRef = useRef(null); useEffect(() => { @@ -1334,6 +1342,393 @@ export function SettingsView() { + {/* Keyboard Shortcuts Section */} +
+
+
+ +

+ Keyboard Shortcuts +

+
+

+ Customize keyboard shortcuts for navigation and actions. Click + on any shortcut to edit it. +

+
+
+ {/* Navigation Shortcuts */} +
+
+

+ Navigation +

+ +
+
+ {[ + { key: "board" as keyof KeyboardShortcuts, label: "Kanban Board" }, + { key: "agent" as keyof KeyboardShortcuts, label: "Agent Runner" }, + { key: "spec" as keyof KeyboardShortcuts, label: "Spec Editor" }, + { key: "context" as keyof KeyboardShortcuts, label: "Context" }, + { key: "tools" as keyof KeyboardShortcuts, label: "Agent Tools" }, + { key: "profiles" as keyof KeyboardShortcuts, label: "AI Profiles" }, + { key: "settings" as keyof KeyboardShortcuts, label: "Settings" }, + ].map(({ key, label }) => ( +
+ {label} +
+ {editingShortcut === key ? ( + <> + { + const value = e.target.value.toUpperCase(); + setShortcutValue(value); + // Check for conflicts + const conflict = Object.entries(keyboardShortcuts).find( + ([k, v]) => k !== key && v.toUpperCase() === value + ); + if (conflict) { + setShortcutError(`Already used by ${conflict[0]}`); + } else { + setShortcutError(null); + } + }} + onKeyDown={(e) => { + if (e.key === "Enter" && !shortcutError && shortcutValue) { + setKeyboardShortcut(key, shortcutValue); + setEditingShortcut(null); + setShortcutValue(""); + setShortcutError(null); + } else if (e.key === "Escape") { + setEditingShortcut(null); + setShortcutValue(""); + setShortcutError(null); + } + }} + className="w-20 h-8 text-center font-mono" + placeholder="Key" + maxLength={1} + autoFocus + data-testid={`edit-shortcut-${key}`} + /> + + + + ) : ( + <> + + {keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] && ( + (modified) + )} + + )} +
+
+ ))} +
+ {shortcutError && ( +

{shortcutError}

+ )} +
+ + {/* UI Shortcuts */} +
+

+ UI Controls +

+
+ {[ + { key: "toggleSidebar" as keyof KeyboardShortcuts, label: "Toggle Sidebar" }, + ].map(({ key, label }) => ( +
+ {label} +
+ {editingShortcut === key ? ( + <> + { + const value = e.target.value; + setShortcutValue(value); + // Check for conflicts + const conflict = Object.entries(keyboardShortcuts).find( + ([k, v]) => k !== key && v === value + ); + if (conflict) { + setShortcutError(`Already used by ${conflict[0]}`); + } else { + setShortcutError(null); + } + }} + onKeyDown={(e) => { + if (e.key === "Enter" && !shortcutError && shortcutValue) { + setKeyboardShortcut(key, shortcutValue); + setEditingShortcut(null); + setShortcutValue(""); + setShortcutError(null); + } else if (e.key === "Escape") { + setEditingShortcut(null); + setShortcutValue(""); + setShortcutError(null); + } + }} + className="w-20 h-8 text-center font-mono" + placeholder="Key" + maxLength={1} + autoFocus + data-testid={`edit-shortcut-${key}`} + /> + + + + ) : ( + <> + + {keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] && ( + (modified) + )} + + )} +
+
+ ))} +
+
+ + {/* Action Shortcuts */} +
+

+ Actions +

+
+ {[ + { key: "addFeature" as keyof KeyboardShortcuts, label: "Add Feature" }, + { key: "addContextFile" as keyof KeyboardShortcuts, label: "Add Context File" }, + { key: "startNext" as keyof KeyboardShortcuts, label: "Start Next Features" }, + { key: "newSession" as keyof KeyboardShortcuts, label: "New Session" }, + { key: "openProject" as keyof KeyboardShortcuts, label: "Open Project" }, + { key: "projectPicker" as keyof KeyboardShortcuts, label: "Project Picker" }, + { key: "cyclePrevProject" as keyof KeyboardShortcuts, label: "Previous Project" }, + { key: "cycleNextProject" as keyof KeyboardShortcuts, label: "Next Project" }, + { key: "addProfile" as keyof KeyboardShortcuts, label: "Add Profile" }, + ].map(({ key, label }) => ( +
+ {label} +
+ {editingShortcut === key ? ( + <> + { + const value = e.target.value.toUpperCase(); + setShortcutValue(value); + // Check for conflicts + const conflict = Object.entries(keyboardShortcuts).find( + ([k, v]) => k !== key && v.toUpperCase() === value + ); + if (conflict) { + setShortcutError(`Already used by ${conflict[0]}`); + } else { + setShortcutError(null); + } + }} + onKeyDown={(e) => { + if (e.key === "Enter" && !shortcutError && shortcutValue) { + setKeyboardShortcut(key, shortcutValue); + setEditingShortcut(null); + setShortcutValue(""); + setShortcutError(null); + } else if (e.key === "Escape") { + setEditingShortcut(null); + setShortcutValue(""); + setShortcutError(null); + } + }} + className="w-20 h-8 text-center font-mono" + placeholder="Key" + maxLength={1} + autoFocus + data-testid={`edit-shortcut-${key}`} + /> + + + + ) : ( + <> + + {keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] && ( + (modified) + )} + + )} +
+
+ ))} +
+
+ + {/* Information */} +
+ +
+

+ About Keyboard Shortcuts +

+

+ Shortcuts won't trigger when typing in input fields. Use + single keys (A-Z, 0-9) or special keys like ` (backtick). + Changes take effect immediately. +

+
+
+
+
+ {/* Feature Defaults Section */}