Add keyboard shortcuts store and update components to use customizable shortcuts

Co-authored-by: GTheMachine <156854865+GTheMachine@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-10 17:31:31 +00:00
committed by Kacper
parent 13f68cba4c
commit e5095c7911
5 changed files with 141 additions and 75 deletions

View File

@@ -64,9 +64,7 @@ import {
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
useKeyboardShortcuts, useKeyboardShortcuts,
NAV_SHORTCUTS, useKeyboardShortcutsConfig,
UI_SHORTCUTS,
ACTION_SHORTCUTS,
KeyboardShortcut, KeyboardShortcut,
} from "@/hooks/use-keyboard-shortcuts"; } from "@/hooks/use-keyboard-shortcuts";
import { getElectronAPI, Project, TrashedProject } from "@/lib/electron"; import { getElectronAPI, Project, TrashedProject } from "@/lib/electron";
@@ -221,6 +219,9 @@ export function Sidebar() {
theme: globalTheme, theme: globalTheme,
} = useAppStore(); } = useAppStore();
// Get customizable keyboard shortcuts
const shortcuts = useKeyboardShortcutsConfig();
// State for project picker dropdown // State for project picker dropdown
const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false); const [isProjectPickerOpen, setIsProjectPickerOpen] = useState(false);
const [showTrashDialog, setShowTrashDialog] = useState(false); const [showTrashDialog, setShowTrashDialog] = useState(false);
@@ -496,13 +497,13 @@ export function Sidebar() {
id: "board", id: "board",
label: "Kanban Board", label: "Kanban Board",
icon: LayoutGrid, icon: LayoutGrid,
shortcut: NAV_SHORTCUTS.board, shortcut: shortcuts.board,
}, },
{ {
id: "agent", id: "agent",
label: "Agent Runner", label: "Agent Runner",
icon: Bot, icon: Bot,
shortcut: NAV_SHORTCUTS.agent, shortcut: shortcuts.agent,
}, },
], ],
}, },
@@ -513,25 +514,25 @@ export function Sidebar() {
id: "spec", id: "spec",
label: "Spec Editor", label: "Spec Editor",
icon: FileText, icon: FileText,
shortcut: NAV_SHORTCUTS.spec, shortcut: shortcuts.spec,
}, },
{ {
id: "context", id: "context",
label: "Context", label: "Context",
icon: BookOpen, icon: BookOpen,
shortcut: NAV_SHORTCUTS.context, shortcut: shortcuts.context,
}, },
{ {
id: "tools", id: "tools",
label: "Agent Tools", label: "Agent Tools",
icon: Wrench, icon: Wrench,
shortcut: NAV_SHORTCUTS.tools, shortcut: shortcuts.tools,
}, },
{ {
id: "profiles", id: "profiles",
label: "AI Profiles", label: "AI Profiles",
icon: UserCircle, icon: UserCircle,
shortcut: NAV_SHORTCUTS.profiles, shortcut: shortcuts.profiles,
}, },
], ],
}, },
@@ -573,26 +574,26 @@ export function Sidebar() {
// Build keyboard shortcuts for navigation // Build keyboard shortcuts for navigation
const navigationShortcuts: KeyboardShortcut[] = useMemo(() => { const navigationShortcuts: KeyboardShortcut[] = useMemo(() => {
const shortcuts: KeyboardShortcut[] = []; const shortcutsList: KeyboardShortcut[] = [];
// Sidebar toggle shortcut - always available // Sidebar toggle shortcut - always available
shortcuts.push({ shortcutsList.push({
key: UI_SHORTCUTS.toggleSidebar, key: shortcuts.toggleSidebar,
action: () => toggleSidebar(), action: () => toggleSidebar(),
description: "Toggle sidebar", description: "Toggle sidebar",
}); });
// Open project shortcut - opens the folder selection dialog directly // Open project shortcut - opens the folder selection dialog directly
shortcuts.push({ shortcutsList.push({
key: ACTION_SHORTCUTS.openProject, key: shortcuts.openProject,
action: () => handleOpenFolder(), action: () => handleOpenFolder(),
description: "Open folder selection dialog", description: "Open folder selection dialog",
}); });
// Project picker shortcut - only when we have projects // Project picker shortcut - only when we have projects
if (projects.length > 0) { if (projects.length > 0) {
shortcuts.push({ shortcutsList.push({
key: ACTION_SHORTCUTS.projectPicker, key: shortcuts.projectPicker,
action: () => setIsProjectPickerOpen((prev) => !prev), action: () => setIsProjectPickerOpen((prev) => !prev),
description: "Toggle project picker", description: "Toggle project picker",
}); });
@@ -600,13 +601,13 @@ export function Sidebar() {
// Project cycling shortcuts - only when we have project history // Project cycling shortcuts - only when we have project history
if (projectHistory.length > 1) { if (projectHistory.length > 1) {
shortcuts.push({ shortcutsList.push({
key: ACTION_SHORTCUTS.cyclePrevProject, key: shortcuts.cyclePrevProject,
action: () => cyclePrevProject(), action: () => cyclePrevProject(),
description: "Cycle to previous project (MRU)", description: "Cycle to previous project (MRU)",
}); });
shortcuts.push({ shortcutsList.push({
key: ACTION_SHORTCUTS.cycleNextProject, key: shortcuts.cycleNextProject,
action: () => cycleNextProject(), action: () => cycleNextProject(),
description: "Cycle to next project (LRU)", description: "Cycle to next project (LRU)",
}); });
@@ -617,7 +618,7 @@ export function Sidebar() {
navSections.forEach((section) => { navSections.forEach((section) => {
section.items.forEach((item) => { section.items.forEach((item) => {
if (item.shortcut) { if (item.shortcut) {
shortcuts.push({ shortcutsList.push({
key: item.shortcut, key: item.shortcut,
action: () => setCurrentView(item.id as any), action: () => setCurrentView(item.id as any),
description: `Navigate to ${item.label}`, description: `Navigate to ${item.label}`,
@@ -627,15 +628,16 @@ export function Sidebar() {
}); });
// Add settings shortcut // Add settings shortcut
shortcuts.push({ shortcutsList.push({
key: NAV_SHORTCUTS.settings, key: shortcuts.settings,
action: () => setCurrentView("settings"), action: () => setCurrentView("settings"),
description: "Navigate to Settings", description: "Navigate to Settings",
}); });
} }
return shortcuts; return shortcutsList;
}, [ }, [
shortcuts,
currentProject, currentProject,
setCurrentView, setCurrentView,
toggleSidebar, toggleSidebar,
@@ -644,6 +646,7 @@ export function Sidebar() {
projectHistory.length, projectHistory.length,
cyclePrevProject, cyclePrevProject,
cycleNextProject, cycleNextProject,
navSections,
]); ]);
// Register keyboard shortcuts // 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" 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" data-testid="sidebar-toggle-shortcut"
> >
{UI_SHORTCUTS.toggleSidebar} {shortcuts.toggleSidebar}
</span> </span>
</div> </div>
</button> </button>
@@ -735,12 +738,12 @@ export function Sidebar() {
<button <button
onClick={handleOpenFolder} onClick={handleOpenFolder}
className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border" className="group flex items-center justify-center flex-1 px-3 py-2.5 rounded-lg relative overflow-hidden transition-all text-muted-foreground hover:text-foreground hover:bg-sidebar-accent/50 border border-sidebar-border"
title={`Open Folder (${ACTION_SHORTCUTS.openProject})`} title={`Open Folder (${shortcuts.openProject})`}
data-testid="open-project-button" data-testid="open-project-button"
> >
<FolderOpen className="w-4 h-4 shrink-0" /> <FolderOpen className="w-4 h-4 shrink-0" />
<span className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70 ml-2"> <span className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70 ml-2">
{ACTION_SHORTCUTS.openProject} {shortcuts.openProject}
</span> </span>
</button> </button>
<button <button
@@ -782,7 +785,7 @@ export function Sidebar() {
className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70" className="hidden lg:flex items-center justify-center w-5 h-5 text-[10px] font-mono rounded bg-brand-500/10 border border-brand-500/30 text-brand-400/70"
data-testid="project-picker-shortcut" data-testid="project-picker-shortcut"
> >
{ACTION_SHORTCUTS.projectPicker} {shortcuts.projectPicker}
</span> </span>
<ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" /> <ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />
</div> </div>
@@ -889,14 +892,14 @@ export function Sidebar() {
<Undo2 className="w-4 h-4 mr-2" /> <Undo2 className="w-4 h-4 mr-2" />
<span className="flex-1">Previous</span> <span className="flex-1">Previous</span>
<span className="text-[10px] font-mono text-muted-foreground ml-2"> <span className="text-[10px] font-mono text-muted-foreground ml-2">
{ACTION_SHORTCUTS.cyclePrevProject} {shortcuts.cyclePrevProject}
</span> </span>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={cycleNextProject} data-testid="cycle-next-project"> <DropdownMenuItem onClick={cycleNextProject} data-testid="cycle-next-project">
<Redo2 className="w-4 h-4 mr-2" /> <Redo2 className="w-4 h-4 mr-2" />
<span className="flex-1">Next</span> <span className="flex-1">Next</span>
<span className="text-[10px] font-mono text-muted-foreground ml-2"> <span className="text-[10px] font-mono text-muted-foreground ml-2">
{ACTION_SHORTCUTS.cycleNextProject} {shortcuts.cycleNextProject}
</span> </span>
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem onClick={clearProjectHistory} data-testid="clear-project-history"> <DropdownMenuItem onClick={clearProjectHistory} data-testid="clear-project-history">
@@ -1052,7 +1055,7 @@ export function Sidebar() {
)} )}
data-testid="shortcut-settings" data-testid="shortcut-settings"
> >
{NAV_SHORTCUTS.settings} {shortcuts.settings}
</span> </span>
)} )}
{!sidebarOpen && ( {!sidebarOpen && (

View File

@@ -24,7 +24,8 @@ import {
} from "lucide-react"; } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { SessionListItem } from "@/types/electron"; import type { SessionListItem } from "@/types/electron";
import { ACTION_SHORTCUTS } from "@/hooks/use-keyboard-shortcuts"; import { useKeyboardShortcutsConfig } from "@/hooks/use-keyboard-shortcuts";
import { useAppStore } from "@/store/app-store";
// Random session name generator // Random session name generator
const adjectives = [ const adjectives = [
@@ -61,6 +62,7 @@ export function SessionManager({
isCurrentSessionThinking = false, isCurrentSessionThinking = false,
onQuickCreateRef, onQuickCreateRef,
}: SessionManagerProps) { }: SessionManagerProps) {
const shortcuts = useKeyboardShortcutsConfig();
const [sessions, setSessions] = useState<SessionListItem[]>([]); const [sessions, setSessions] = useState<SessionListItem[]>([]);
const [activeTab, setActiveTab] = useState<"active" | "archived">("active"); const [activeTab, setActiveTab] = useState<"active" | "archived">("active");
const [editingSessionId, setEditingSessionId] = useState<string | null>(null); const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
@@ -246,12 +248,12 @@ export function SessionManager({
size="sm" size="sm"
onClick={handleQuickCreateSession} onClick={handleQuickCreateSession}
data-testid="new-session-button" data-testid="new-session-button"
title={`New Session (${ACTION_SHORTCUTS.newSession})`} title={`New Session (${shortcuts.newSession})`}
> >
<Plus className="w-4 h-4 mr-1" /> <Plus className="w-4 h-4 mr-1" />
New New
<span className="ml-1.5 px-1.5 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/20 border border-primary-foreground/30 text-primary-foreground"> <span className="ml-1.5 px-1.5 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/20 border border-primary-foreground/30 text-primary-foreground">
{ACTION_SHORTCUTS.newSession} {shortcuts.newSession}
</span> </span>
</Button> </Button>
)} )}

View File

@@ -86,7 +86,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { useAutoMode } from "@/hooks/use-auto-mode"; import { useAutoMode } from "@/hooks/use-auto-mode";
import { import {
useKeyboardShortcuts, useKeyboardShortcuts,
ACTION_SHORTCUTS, useKeyboardShortcutsConfig,
KeyboardShortcut, KeyboardShortcut,
} from "@/hooks/use-keyboard-shortcuts"; } from "@/hooks/use-keyboard-shortcuts";
import { useWindowState } from "@/hooks/use-window-state"; import { useWindowState } from "@/hooks/use-window-state";
@@ -189,6 +189,7 @@ export function BoardView() {
showProfilesOnly, showProfilesOnly,
aiProfiles, aiProfiles,
} = useAppStore(); } = useAppStore();
const shortcuts = useKeyboardShortcutsConfig();
const [activeFeature, setActiveFeature] = useState<Feature | null>(null); const [activeFeature, setActiveFeature] = useState<Feature | null>(null);
const [editingFeature, setEditingFeature] = useState<Feature | null>(null); const [editingFeature, setEditingFeature] = useState<Feature | null>(null);
const [showAddDialog, setShowAddDialog] = useState(false); const [showAddDialog, setShowAddDialog] = useState(false);
@@ -292,14 +293,14 @@ export function BoardView() {
// Keyboard shortcuts for this view // Keyboard shortcuts for this view
const boardShortcuts: KeyboardShortcut[] = useMemo(() => { const boardShortcuts: KeyboardShortcut[] = useMemo(() => {
const shortcuts: KeyboardShortcut[] = [ const shortcutsList: KeyboardShortcut[] = [
{ {
key: ACTION_SHORTCUTS.addFeature, key: shortcuts.addFeature,
action: () => setShowAddDialog(true), action: () => setShowAddDialog(true),
description: "Add new feature", description: "Add new feature",
}, },
{ {
key: ACTION_SHORTCUTS.startNext, key: shortcuts.startNext,
action: () => startNextFeaturesRef.current(), action: () => startNextFeaturesRef.current(),
description: "Start next features from backlog", description: "Start next features from backlog",
}, },
@@ -309,7 +310,7 @@ export function BoardView() {
inProgressFeaturesForShortcuts.slice(0, 10).forEach((feature, index) => { inProgressFeaturesForShortcuts.slice(0, 10).forEach((feature, index) => {
// Keys 1-9 for first 9 cards, 0 for 10th card // Keys 1-9 for first 9 cards, 0 for 10th card
const key = index === 9 ? "0" : String(index + 1); const key = index === 9 ? "0" : String(index + 1);
shortcuts.push({ shortcutsList.push({
key, key,
action: () => { action: () => {
setOutputFeature(feature); setOutputFeature(feature);
@@ -319,8 +320,8 @@ export function BoardView() {
}); });
}); });
return shortcuts; return shortcutsList;
}, [inProgressFeaturesForShortcuts]); }, [inProgressFeaturesForShortcuts, shortcuts]);
useKeyboardShortcuts(boardShortcuts); useKeyboardShortcuts(boardShortcuts);
// Prevent hydration issues // 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" 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" data-testid="shortcut-add-feature"
> >
{ACTION_SHORTCUTS.addFeature} {shortcuts.addFeature}
</span> </span>
</Button> </Button>
</div> </div>
@@ -1636,7 +1637,7 @@ export function BoardView() {
<FastForward className="w-3 h-3 mr-1" /> <FastForward className="w-3 h-3 mr-1" />
Start Next Start Next
<span className="ml-1 px-1 py-0.5 text-[9px] font-mono rounded bg-accent border border-border-glass"> <span className="ml-1 px-1 py-0.5 text-[9px] font-mono rounded bg-accent border border-border-glass">
{ACTION_SHORTCUTS.startNext} {shortcuts.startNext}
</span> </span>
</Button> </Button>
)} )}

View File

@@ -1,6 +1,7 @@
"use client"; "use client";
import { useEffect, useCallback } from "react"; import { useEffect, useCallback } from "react";
import { useAppStore } from "@/store/app-store";
export interface KeyboardShortcut { export interface KeyboardShortcut {
key: string; 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<string, string> = { export function useKeyboardShortcutsConfig() {
board: "K", // K for Kanban const keyboardShortcuts = useAppStore((state) => state.keyboardShortcuts);
agent: "A", // A for Agent return keyboardShortcuts;
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<string, string> = {
toggleSidebar: "`", // Backtick to toggle sidebar
};
/**
* Shortcut definitions for add buttons
*/
export const ACTION_SHORTCUTS: Record<string, string> = {
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)
};

View File

@@ -37,6 +37,58 @@ export interface ApiKeys {
openai: string; 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 { export interface ImageAttachment {
id: string; id: string;
data: string; // base64 encoded image data data: string; // base64 encoded image data
@@ -203,6 +255,9 @@ export interface AppState {
// Profile Display Settings // Profile Display Settings
showProfilesOnly: boolean; // When true, hide model tweaking options and show only profile selection showProfilesOnly: boolean; // When true, hide model tweaking options and show only profile selection
// Keyboard Shortcuts
keyboardShortcuts: KeyboardShortcuts; // User-defined keyboard shortcuts
// Project Analysis // Project Analysis
projectAnalysis: ProjectAnalysis | null; projectAnalysis: ProjectAnalysis | null;
isAnalyzing: boolean; isAnalyzing: boolean;
@@ -303,6 +358,11 @@ export interface AppActions {
// Profile Display Settings actions // Profile Display Settings actions
setShowProfilesOnly: (enabled: boolean) => void; setShowProfilesOnly: (enabled: boolean) => void;
// Keyboard Shortcuts actions
setKeyboardShortcut: (key: keyof KeyboardShortcuts, value: string) => void;
setKeyboardShortcuts: (shortcuts: Partial<KeyboardShortcuts>) => void;
resetKeyboardShortcuts: () => void;
// AI Profile actions // AI Profile actions
addAIProfile: (profile: Omit<AIProfile, "id">) => void; addAIProfile: (profile: Omit<AIProfile, "id">) => void;
updateAIProfile: (id: string, updates: Partial<AIProfile>) => void; updateAIProfile: (id: string, updates: Partial<AIProfile>) => void;
@@ -404,6 +464,7 @@ const initialState: AppState = {
defaultSkipTests: false, // Default to TDD mode (tests enabled) defaultSkipTests: false, // Default to TDD mode (tests enabled)
useWorktrees: false, // Default to disabled (worktree feature is experimental) useWorktrees: false, // Default to disabled (worktree feature is experimental)
showProfilesOnly: false, // Default to showing all options (not profiles only) showProfilesOnly: false, // Default to showing all options (not profiles only)
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts
aiProfiles: DEFAULT_AI_PROFILES, aiProfiles: DEFAULT_AI_PROFILES,
projectAnalysis: null, projectAnalysis: null,
isAnalyzing: false, isAnalyzing: false,
@@ -907,6 +968,29 @@ export const useAppStore = create<AppState & AppActions>()(
// Profile Display Settings actions // Profile Display Settings actions
setShowProfilesOnly: (enabled) => set({ showProfilesOnly: enabled }), 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 // AI Profile actions
addAIProfile: (profile) => { addAIProfile: (profile) => {
const id = `profile-${Date.now()}-${Math.random() const id = `profile-${Date.now()}-${Math.random()
@@ -985,6 +1069,7 @@ export const useAppStore = create<AppState & AppActions>()(
defaultSkipTests: state.defaultSkipTests, defaultSkipTests: state.defaultSkipTests,
useWorktrees: state.useWorktrees, useWorktrees: state.useWorktrees,
showProfilesOnly: state.showProfilesOnly, showProfilesOnly: state.showProfilesOnly,
keyboardShortcuts: state.keyboardShortcuts,
aiProfiles: state.aiProfiles, aiProfiles: state.aiProfiles,
lastSelectedSessionByProject: state.lastSelectedSessionByProject, lastSelectedSessionByProject: state.lastSelectedSessionByProject,
}), }),