diff --git a/app/src/components/layout/sidebar.tsx b/app/src/components/layout/sidebar.tsx index 77e92c28..15046e98 100644 --- a/app/src/components/layout/sidebar.tsx +++ b/app/src/components/layout/sidebar.tsx @@ -64,9 +64,7 @@ import { import { Button } from "@/components/ui/button"; import { useKeyboardShortcuts, - NAV_SHORTCUTS, - UI_SHORTCUTS, - ACTION_SHORTCUTS, + useKeyboardShortcutsConfig, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; import { getElectronAPI, Project, TrashedProject } from "@/lib/electron"; @@ -221,6 +219,9 @@ export function Sidebar() { theme: globalTheme, } = useAppStore(); + // Get customizable keyboard shortcuts + const shortcuts = useKeyboardShortcutsConfig(); + // State for project picker dropdown const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false); const [showTrashDialog, setShowTrashDialog] = useState(false); @@ -496,13 +497,13 @@ export function Sidebar() { id: "board", label: "Kanban Board", icon: LayoutGrid, - shortcut: NAV_SHORTCUTS.board, + shortcut: shortcuts.board, }, { id: "agent", label: "Agent Runner", icon: Bot, - shortcut: NAV_SHORTCUTS.agent, + shortcut: shortcuts.agent, }, ], }, @@ -513,25 +514,25 @@ export function Sidebar() { id: "spec", label: "Spec Editor", icon: FileText, - shortcut: NAV_SHORTCUTS.spec, + shortcut: shortcuts.spec, }, { id: "context", label: "Context", icon: BookOpen, - shortcut: NAV_SHORTCUTS.context, + shortcut: shortcuts.context, }, { id: "tools", label: "Agent Tools", icon: Wrench, - shortcut: NAV_SHORTCUTS.tools, + shortcut: shortcuts.tools, }, { id: "profiles", label: "AI Profiles", icon: UserCircle, - shortcut: NAV_SHORTCUTS.profiles, + shortcut: shortcuts.profiles, }, ], }, @@ -573,26 +574,26 @@ export function Sidebar() { // Build keyboard shortcuts for navigation const navigationShortcuts: KeyboardShortcut[] = useMemo(() => { - const shortcuts: KeyboardShortcut[] = []; + const shortcutsList: KeyboardShortcut[] = []; // Sidebar toggle shortcut - always available - shortcuts.push({ - key: UI_SHORTCUTS.toggleSidebar, + shortcutsList.push({ + key: shortcuts.toggleSidebar, action: () => toggleSidebar(), description: "Toggle sidebar", }); // Open project shortcut - opens the folder selection dialog directly - shortcuts.push({ - key: ACTION_SHORTCUTS.openProject, + shortcutsList.push({ + key: shortcuts.openProject, action: () => handleOpenFolder(), description: "Open folder selection dialog", }); // Project picker shortcut - only when we have projects if (projects.length > 0) { - shortcuts.push({ - key: ACTION_SHORTCUTS.projectPicker, + shortcutsList.push({ + key: shortcuts.projectPicker, action: () => setIsProjectPickerOpen((prev) => !prev), description: "Toggle project picker", }); @@ -600,13 +601,13 @@ export function Sidebar() { // Project cycling shortcuts - only when we have project history if (projectHistory.length > 1) { - shortcuts.push({ - key: ACTION_SHORTCUTS.cyclePrevProject, + shortcutsList.push({ + key: shortcuts.cyclePrevProject, action: () => cyclePrevProject(), description: "Cycle to previous project (MRU)", }); - shortcuts.push({ - key: ACTION_SHORTCUTS.cycleNextProject, + shortcutsList.push({ + key: shortcuts.cycleNextProject, action: () => cycleNextProject(), description: "Cycle to next project (LRU)", }); @@ -617,7 +618,7 @@ export function Sidebar() { navSections.forEach((section) => { section.items.forEach((item) => { if (item.shortcut) { - shortcuts.push({ + shortcutsList.push({ key: item.shortcut, action: () => setCurrentView(item.id as any), description: `Navigate to ${item.label}`, @@ -627,15 +628,16 @@ export function Sidebar() { }); // Add settings shortcut - shortcuts.push({ - key: NAV_SHORTCUTS.settings, + shortcutsList.push({ + key: shortcuts.settings, action: () => setCurrentView("settings"), description: "Navigate to Settings", }); } - return shortcuts; + return shortcutsList; }, [ + shortcuts, currentProject, setCurrentView, toggleSidebar, @@ -644,6 +646,7 @@ export function Sidebar() { projectHistory.length, cyclePrevProject, cycleNextProject, + navSections, ]); // Register keyboard shortcuts @@ -682,7 +685,7 @@ export function Sidebar() { className="ml-1 px-1 py-0.5 bg-brand-500/10 border border-brand-500/30 rounded text-[10px] font-mono text-brand-400/70" data-testid="sidebar-toggle-shortcut" > - {UI_SHORTCUTS.toggleSidebar} + {shortcuts.toggleSidebar} @@ -735,12 +738,12 @@ export function Sidebar() { )} diff --git a/app/src/components/views/board-view.tsx b/app/src/components/views/board-view.tsx index 84fcf2be..ab504986 100644 --- a/app/src/components/views/board-view.tsx +++ b/app/src/components/views/board-view.tsx @@ -86,7 +86,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { useAutoMode } from "@/hooks/use-auto-mode"; import { useKeyboardShortcuts, - ACTION_SHORTCUTS, + useKeyboardShortcutsConfig, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; import { useWindowState } from "@/hooks/use-window-state"; @@ -189,6 +189,7 @@ export function BoardView() { showProfilesOnly, aiProfiles, } = useAppStore(); + const shortcuts = useKeyboardShortcutsConfig(); const [activeFeature, setActiveFeature] = useState(null); const [editingFeature, setEditingFeature] = useState(null); const [showAddDialog, setShowAddDialog] = useState(false); @@ -292,14 +293,14 @@ export function BoardView() { // Keyboard shortcuts for this view const boardShortcuts: KeyboardShortcut[] = useMemo(() => { - const shortcuts: KeyboardShortcut[] = [ + const shortcutsList: KeyboardShortcut[] = [ { - key: ACTION_SHORTCUTS.addFeature, + key: shortcuts.addFeature, action: () => setShowAddDialog(true), description: "Add new feature", }, { - key: ACTION_SHORTCUTS.startNext, + key: shortcuts.startNext, action: () => startNextFeaturesRef.current(), description: "Start next features from backlog", }, @@ -309,7 +310,7 @@ export function BoardView() { inProgressFeaturesForShortcuts.slice(0, 10).forEach((feature, index) => { // Keys 1-9 for first 9 cards, 0 for 10th card const key = index === 9 ? "0" : String(index + 1); - shortcuts.push({ + shortcutsList.push({ key, action: () => { setOutputFeature(feature); @@ -319,8 +320,8 @@ export function BoardView() { }); }); - return shortcuts; - }, [inProgressFeaturesForShortcuts]); + return shortcutsList; + }, [inProgressFeaturesForShortcuts, shortcuts]); useKeyboardShortcuts(boardShortcuts); // Prevent hydration issues @@ -1567,7 +1568,7 @@ export function BoardView() { className="ml-3 px-2 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/20 border border-primary-foreground/30 text-primary-foreground inline-flex items-center justify-center" data-testid="shortcut-add-feature" > - {ACTION_SHORTCUTS.addFeature} + {shortcuts.addFeature} @@ -1636,7 +1637,7 @@ export function BoardView() { Start Next - {ACTION_SHORTCUTS.startNext} + {shortcuts.startNext} )} diff --git a/app/src/hooks/use-keyboard-shortcuts.ts b/app/src/hooks/use-keyboard-shortcuts.ts index 9622d10f..a9b901ff 100644 --- a/app/src/hooks/use-keyboard-shortcuts.ts +++ b/app/src/hooks/use-keyboard-shortcuts.ts @@ -1,6 +1,7 @@ "use client"; import { useEffect, useCallback } from "react"; +import { useAppStore } from "@/store/app-store"; export interface KeyboardShortcut { key: string; @@ -97,36 +98,10 @@ export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) { } /** - * Shortcut definitions for navigation + * Hook to get current keyboard shortcuts from store + * This replaces the static constants and allows customization */ -export const NAV_SHORTCUTS: Record = { - board: "K", // K for Kanban - agent: "A", // A for Agent - spec: "D", // D for Document (Spec) - context: "C", // C for Context - tools: "T", // T for Tools - settings: "S", // S for Settings - profiles: "M", // M for Models/profiles -}; - -/** - * Shortcut definitions for UI controls - */ -export const UI_SHORTCUTS: Record = { - toggleSidebar: "`", // Backtick to toggle sidebar -}; - -/** - * Shortcut definitions for add buttons - */ -export const ACTION_SHORTCUTS: Record = { - addFeature: "N", // N for New feature - addContextFile: "F", // F for File (add context file) - startNext: "G", // G for Grab (start next features from backlog) - newSession: "N", // N for New session (in agent view) - openProject: "O", // O for Open project (navigate to welcome view) - projectPicker: "P", // P for Project picker - cyclePrevProject: "Q", // Q for previous project (cycle back through MRU) - cycleNextProject: "E", // E for next project (cycle forward through MRU) - addProfile: "N", // N for New profile (when in profiles view) -}; +export function useKeyboardShortcutsConfig() { + const keyboardShortcuts = useAppStore((state) => state.keyboardShortcuts); + return keyboardShortcuts; +} diff --git a/app/src/store/app-store.ts b/app/src/store/app-store.ts index 738de87f..77779ecb 100644 --- a/app/src/store/app-store.ts +++ b/app/src/store/app-store.ts @@ -37,6 +37,58 @@ export interface ApiKeys { openai: string; } +// Keyboard Shortcuts +export interface KeyboardShortcuts { + // Navigation shortcuts + board: string; + agent: string; + spec: string; + context: string; + tools: string; + settings: string; + profiles: string; + + // UI shortcuts + toggleSidebar: string; + + // Action shortcuts + addFeature: string; + addContextFile: string; + startNext: string; + newSession: string; + openProject: string; + projectPicker: string; + cyclePrevProject: string; + cycleNextProject: string; + addProfile: string; +} + +// Default keyboard shortcuts +export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = { + // Navigation + board: "K", + agent: "A", + spec: "D", + context: "C", + tools: "T", + settings: "S", + profiles: "M", + + // UI + toggleSidebar: "`", + + // Actions + addFeature: "N", + addContextFile: "F", + startNext: "G", + newSession: "N", + openProject: "O", + projectPicker: "P", + cyclePrevProject: "Q", + cycleNextProject: "E", + addProfile: "N", +}; + export interface ImageAttachment { id: string; data: string; // base64 encoded image data @@ -203,6 +255,9 @@ export interface AppState { // Profile Display Settings showProfilesOnly: boolean; // When true, hide model tweaking options and show only profile selection + // Keyboard Shortcuts + keyboardShortcuts: KeyboardShortcuts; // User-defined keyboard shortcuts + // Project Analysis projectAnalysis: ProjectAnalysis | null; isAnalyzing: boolean; @@ -303,6 +358,11 @@ export interface AppActions { // Profile Display Settings actions setShowProfilesOnly: (enabled: boolean) => void; + // Keyboard Shortcuts actions + setKeyboardShortcut: (key: keyof KeyboardShortcuts, value: string) => void; + setKeyboardShortcuts: (shortcuts: Partial) => void; + resetKeyboardShortcuts: () => void; + // AI Profile actions addAIProfile: (profile: Omit) => void; updateAIProfile: (id: string, updates: Partial) => void; @@ -404,6 +464,7 @@ const initialState: AppState = { defaultSkipTests: false, // Default to TDD mode (tests enabled) useWorktrees: false, // Default to disabled (worktree feature is experimental) showProfilesOnly: false, // Default to showing all options (not profiles only) + keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts aiProfiles: DEFAULT_AI_PROFILES, projectAnalysis: null, isAnalyzing: false, @@ -907,6 +968,29 @@ export const useAppStore = create()( // Profile Display Settings actions setShowProfilesOnly: (enabled) => set({ showProfilesOnly: enabled }), + // Keyboard Shortcuts actions + setKeyboardShortcut: (key, value) => { + set({ + keyboardShortcuts: { + ...get().keyboardShortcuts, + [key]: value, + }, + }); + }, + + setKeyboardShortcuts: (shortcuts) => { + set({ + keyboardShortcuts: { + ...get().keyboardShortcuts, + ...shortcuts, + }, + }); + }, + + resetKeyboardShortcuts: () => { + set({ keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS }); + }, + // AI Profile actions addAIProfile: (profile) => { const id = `profile-${Date.now()}-${Math.random() @@ -985,6 +1069,7 @@ export const useAppStore = create()( defaultSkipTests: state.defaultSkipTests, useWorktrees: state.useWorktrees, showProfilesOnly: state.showProfilesOnly, + keyboardShortcuts: state.keyboardShortcuts, aiProfiles: state.aiProfiles, lastSelectedSessionByProject: state.lastSelectedSessionByProject, }),