mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
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:
committed by
Kacper
parent
13f68cba4c
commit
e5095c7911
@@ -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}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
@@ -735,12 +738,12 @@ export function Sidebar() {
|
||||
<button
|
||||
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"
|
||||
title={`Open Folder (${ACTION_SHORTCUTS.openProject})`}
|
||||
title={`Open Folder (${shortcuts.openProject})`}
|
||||
data-testid="open-project-button"
|
||||
>
|
||||
<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">
|
||||
{ACTION_SHORTCUTS.openProject}
|
||||
{shortcuts.openProject}
|
||||
</span>
|
||||
</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"
|
||||
data-testid="project-picker-shortcut"
|
||||
>
|
||||
{ACTION_SHORTCUTS.projectPicker}
|
||||
{shortcuts.projectPicker}
|
||||
</span>
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
</div>
|
||||
@@ -889,14 +892,14 @@ export function Sidebar() {
|
||||
<Undo2 className="w-4 h-4 mr-2" />
|
||||
<span className="flex-1">Previous</span>
|
||||
<span className="text-[10px] font-mono text-muted-foreground ml-2">
|
||||
{ACTION_SHORTCUTS.cyclePrevProject}
|
||||
{shortcuts.cyclePrevProject}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={cycleNextProject} data-testid="cycle-next-project">
|
||||
<Redo2 className="w-4 h-4 mr-2" />
|
||||
<span className="flex-1">Next</span>
|
||||
<span className="text-[10px] font-mono text-muted-foreground ml-2">
|
||||
{ACTION_SHORTCUTS.cycleNextProject}
|
||||
{shortcuts.cycleNextProject}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={clearProjectHistory} data-testid="clear-project-history">
|
||||
@@ -1052,7 +1055,7 @@ export function Sidebar() {
|
||||
)}
|
||||
data-testid="shortcut-settings"
|
||||
>
|
||||
{NAV_SHORTCUTS.settings}
|
||||
{shortcuts.settings}
|
||||
</span>
|
||||
)}
|
||||
{!sidebarOpen && (
|
||||
|
||||
@@ -24,7 +24,8 @@ import {
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
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
|
||||
const adjectives = [
|
||||
@@ -61,6 +62,7 @@ export function SessionManager({
|
||||
isCurrentSessionThinking = false,
|
||||
onQuickCreateRef,
|
||||
}: SessionManagerProps) {
|
||||
const shortcuts = useKeyboardShortcutsConfig();
|
||||
const [sessions, setSessions] = useState<SessionListItem[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<"active" | "archived">("active");
|
||||
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
|
||||
@@ -246,12 +248,12 @@ export function SessionManager({
|
||||
size="sm"
|
||||
onClick={handleQuickCreateSession}
|
||||
data-testid="new-session-button"
|
||||
title={`New Session (${ACTION_SHORTCUTS.newSession})`}
|
||||
title={`New Session (${shortcuts.newSession})`}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
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">
|
||||
{ACTION_SHORTCUTS.newSession}
|
||||
{shortcuts.newSession}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -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<Feature | null>(null);
|
||||
const [editingFeature, setEditingFeature] = useState<Feature | null>(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}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -1636,7 +1637,7 @@ export function BoardView() {
|
||||
<FastForward className="w-3 h-3 mr-1" />
|
||||
Start Next
|
||||
<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>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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<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)
|
||||
};
|
||||
export function useKeyboardShortcutsConfig() {
|
||||
const keyboardShortcuts = useAppStore((state) => state.keyboardShortcuts);
|
||||
return keyboardShortcuts;
|
||||
}
|
||||
|
||||
@@ -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<KeyboardShortcuts>) => void;
|
||||
resetKeyboardShortcuts: () => void;
|
||||
|
||||
// AI Profile actions
|
||||
addAIProfile: (profile: Omit<AIProfile, "id">) => void;
|
||||
updateAIProfile: (id: string, updates: Partial<AIProfile>) => 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<AppState & AppActions>()(
|
||||
// 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<AppState & AppActions>()(
|
||||
defaultSkipTests: state.defaultSkipTests,
|
||||
useWorktrees: state.useWorktrees,
|
||||
showProfilesOnly: state.showProfilesOnly,
|
||||
keyboardShortcuts: state.keyboardShortcuts,
|
||||
aiProfiles: state.aiProfiles,
|
||||
lastSelectedSessionByProject: state.lastSelectedSessionByProject,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user