diff --git a/README.md b/README.md new file mode 100644 index 00000000..d23a9d80 --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# Automaker + +Automaker is an autonomous AI development studio that helps you build software faster using AI-powered agents. It provides a visual Kanban board interface to manage features, automatically assigns AI agents to implement them, and tracks progress through an intuitive workflow from backlog to verified completion. + +--- + +> **[!CAUTION]** +> +> ## Security Disclaimer +> +> **This software uses AI-powered tooling that has access to your operating system and can read, modify, and delete files. Use at your own risk.** +> +> We have reviewed this codebase for security vulnerabilities, but you assume all risk when running this software. You should review the code yourself before running it. +> +> **We do not recommend running Automaker directly on your local computer** due to the risk of AI agents having access to your entire file system. Please sandbox this application using Docker or a virtual machine. +> +> **[Read the full disclaimer](../DISCLAIMER.md)** + +--- + +## Getting Started + +**Step 1:** Clone this repository: + +```bash +git clone git@github.com:AutoMaker-Org/automaker.git +cd automaker +``` + +**Step 2:** Install dependencies: + +```bash +npm install +``` + +**Step 3:** Run the Claude Code setup token command: + +```bash +claude setup-token +``` + +> **โš ๏ธ Warning:** This command will print your token to your terminal. Be careful if you're streaming or sharing your screen, as the token will be visible to anyone watching. + +**Step 4:** Export the Claude Code OAuth token in your shell: + +```bash +export CLAUDE_CODE_OAUTH_TOKEN="your-token-here" +``` + +**Step 5:** Start the development server: + +```bash +npm run dev:electron +``` + +This will start both the Next.js development server and the Electron application. + +**Step 6:** MOST IMPORANT: Run the Following after all is setup + +```bash +echo "W" +echo "W" +echo "W" +echo "W" +echo "W" +echo "W" +echo "W" +echo "W" +``` + +## Features + +- ๐Ÿ“‹ **Kanban Board** - Visual drag-and-drop board to manage features through backlog, in progress, waiting approval, and verified stages +- ๐Ÿค– **AI Agent Integration** - Automatic AI agent assignment to implement features when moved to "In Progress" +- ๐Ÿง  **Multi-Model Support** - Choose from multiple AI models including Claude Opus, Sonnet, and more +- ๐Ÿ’ญ **Extended Thinking** - Enable extended thinking modes for complex problem-solving +- ๐Ÿ“ก **Real-time Agent Output** - View live agent output, logs, and file diffs as features are being implemented +- ๐Ÿ” **Project Analysis** - AI-powered project structure analysis to understand your codebase +- ๐Ÿ“ **Context Management** - Add context files to help AI agents understand your project better +- ๐Ÿ’ก **Feature Suggestions** - AI-generated feature suggestions based on your project +- ๐Ÿ–ผ๏ธ **Image Support** - Attach images and screenshots to feature descriptions +- โšก **Concurrent Processing** - Configure concurrency to process multiple features simultaneously +- ๐Ÿงช **Test Integration** - Automatic test running and verification for implemented features +- ๐Ÿ”€ **Git Integration** - View git diffs and track changes made by AI agents +- ๐Ÿ‘ค **AI Profiles** - Create and manage different AI agent profiles for various tasks +- ๐Ÿ’ฌ **Chat History** - Keep track of conversations and interactions with AI agents +- โŒจ๏ธ **Keyboard Shortcuts** - Efficient navigation and actions via keyboard shortcuts +- ๐ŸŽจ **Dark/Light Theme** - Beautiful UI with theme support +- ๐Ÿ–ฅ๏ธ **Cross-Platform** - Desktop application built with Electron for Windows, macOS, and Linux + +## Tech Stack + +- [Next.js](https://nextjs.org) - React framework +- [Electron](https://www.electronjs.org/) - Desktop application framework +- [Tailwind CSS](https://tailwindcss.com/) - Styling +- [Zustand](https://zustand-demo.pmnd.rs/) - State management +- [dnd-kit](https://dndkit.com/) - Drag and drop functionality + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +## License + +See [LICENSE](../LICENSE) for details. 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/agent-view.tsx b/app/src/components/views/agent-view.tsx index 7e59a123..2d6a7b7c 100644 --- a/app/src/components/views/agent-view.tsx +++ b/app/src/components/views/agent-view.tsx @@ -26,12 +26,13 @@ import { Markdown } from "@/components/ui/markdown"; import type { ImageAttachment } from "@/store/app-store"; import { useKeyboardShortcuts, - ACTION_SHORTCUTS, + useKeyboardShortcutsConfig, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; export function AgentView() { const { currentProject, setLastSelectedSession, getLastSelectedSession } = useAppStore(); + const shortcuts = useKeyboardShortcutsConfig(); const [input, setInput] = useState(""); const [selectedImages, setSelectedImages] = useState([]); const [showImageDropZone, setShowImageDropZone] = useState(false); @@ -417,12 +418,12 @@ export function AgentView() { // Keyboard shortcuts for agent view const agentShortcuts: KeyboardShortcut[] = useMemo(() => { - const shortcuts: KeyboardShortcut[] = []; + const shortcutsList: KeyboardShortcut[] = []; // New session shortcut - only when in agent view with a project if (currentProject) { - shortcuts.push({ - key: ACTION_SHORTCUTS.newSession, + shortcutsList.push({ + key: shortcuts.newSession, action: () => { if (quickCreateSessionRef.current) { quickCreateSessionRef.current(); @@ -432,8 +433,8 @@ export function AgentView() { }); } - return shortcuts; - }, [currentProject]); + return shortcutsList; + }, [currentProject, shortcuts]); // Register keyboard shortcuts useKeyboardShortcuts(agentShortcuts); 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/components/views/context-view.tsx b/app/src/components/views/context-view.tsx index 5cb184a9..b8d1a1ec 100644 --- a/app/src/components/views/context-view.tsx +++ b/app/src/components/views/context-view.tsx @@ -19,7 +19,7 @@ import { } from "lucide-react"; import { useKeyboardShortcuts, - ACTION_SHORTCUTS, + useKeyboardShortcutsConfig, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; import { @@ -43,6 +43,7 @@ interface ContextFile { export function ContextView() { const { currentProject } = useAppStore(); + const shortcuts = useKeyboardShortcutsConfig(); const [contextFiles, setContextFiles] = useState([]); const [selectedFile, setSelectedFile] = useState(null); const [isLoading, setIsLoading] = useState(true); @@ -63,12 +64,12 @@ export function ContextView() { const contextShortcuts: KeyboardShortcut[] = useMemo( () => [ { - key: ACTION_SHORTCUTS.addContextFile, + key: shortcuts.addContextFile, action: () => setIsAddDialogOpen(true), description: "Add new context file", }, ], - [] + [shortcuts] ); useKeyboardShortcuts(contextShortcuts); @@ -374,7 +375,7 @@ export function ContextView() { className="ml-2 px-1.5 py-0.5 text-[10px] font-mono rounded bg-secondary border border-border" data-testid="shortcut-add-context-file" > - {ACTION_SHORTCUTS.addContextFile} + {shortcuts.addContextFile} diff --git a/app/src/components/views/profiles-view.tsx b/app/src/components/views/profiles-view.tsx index 82bf811d..bd882845 100644 --- a/app/src/components/views/profiles-view.tsx +++ b/app/src/components/views/profiles-view.tsx @@ -9,7 +9,7 @@ import { Textarea } from "@/components/ui/textarea"; import { cn, modelSupportsThinking } from "@/lib/utils"; import { useKeyboardShortcuts, - ACTION_SHORTCUTS, + useKeyboardShortcutsConfig, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; import { @@ -440,6 +440,7 @@ function ProfileForm({ export function ProfilesView() { const { aiProfiles, addAIProfile, updateAIProfile, removeAIProfile, reorderAIProfiles } = useAppStore(); + const shortcuts = useKeyboardShortcutsConfig(); const [showAddDialog, setShowAddDialog] = useState(false); const [editingProfile, setEditingProfile] = useState(null); @@ -508,17 +509,17 @@ export function ProfilesView() { // Build keyboard shortcuts for profiles view const profilesShortcuts: KeyboardShortcut[] = useMemo(() => { - const shortcuts: KeyboardShortcut[] = []; + const shortcutsList: KeyboardShortcut[] = []; // Add profile shortcut - when in profiles view - shortcuts.push({ - key: ACTION_SHORTCUTS.addProfile, + shortcutsList.push({ + key: shortcuts.addProfile, action: () => setShowAddDialog(true), description: "Create new profile", }); - return shortcuts; - }, []); + return shortcutsList; + }, [shortcuts]); // Register keyboard shortcuts for profiles view useKeyboardShortcuts(profilesShortcuts); @@ -549,7 +550,7 @@ export function ProfilesView() { New Profile - {ACTION_SHORTCUTS.addProfile} + {shortcuts.addProfile} diff --git a/app/src/components/views/settings-view.tsx b/app/src/components/views/settings-view.tsx index 7a91d602..074d2252 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"; @@ -60,6 +61,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 }, ]; @@ -81,6 +83,9 @@ export function SettingsView() { setShowProfilesOnly, currentProject, moveProjectToTrash, + keyboardShortcuts, + setKeyboardShortcut, + resetKeyboardShortcuts, } = useAppStore(); const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic); const [googleKey, setGoogleKey] = useState(apiKeys.google); @@ -1529,6 +1534,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-24 h-8 text-center font-mono" + placeholder="Key" + maxLength={2} + 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-24 h-8 text-center font-mono" + placeholder="Key" + maxLength={2} + 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-24 h-8 text-center font-mono" + placeholder="Key" + maxLength={2} + 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 */}
= { - 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..7c9ced56 100644 --- a/app/src/store/app-store.ts +++ b/app/src/store/app-store.ts @@ -37,6 +37,60 @@ 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 + // Note: Some shortcuts share the same key (e.g., "N" for addFeature, newSession, addProfile) + // This is intentional as they are context-specific and only active in their respective views + addFeature: "N", // Only active in board view + addContextFile: "F", // Only active in context view + startNext: "G", // Only active in board view + newSession: "N", // Only active in agent view + openProject: "O", // Global shortcut + projectPicker: "P", // Global shortcut + cyclePrevProject: "Q", // Global shortcut + cycleNextProject: "E", // Global shortcut + addProfile: "N", // Only active in profiles view +}; + export interface ImageAttachment { id: string; data: string; // base64 encoded image data @@ -203,6 +257,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 +360,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 +466,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 +970,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 +1071,7 @@ export const useAppStore = create()( defaultSkipTests: state.defaultSkipTests, useWorktrees: state.useWorktrees, showProfilesOnly: state.showProfilesOnly, + keyboardShortcuts: state.keyboardShortcuts, aiProfiles: state.aiProfiles, lastSelectedSessionByProject: state.lastSelectedSessionByProject, }),