mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
Merge branch 'main' into feature/auth-setup-settings-menu
This commit is contained in:
108
README.md
Normal file
108
README.md
Normal file
@@ -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.
|
||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -26,12 +26,13 @@ import { Markdown } from "@/components/ui/markdown";
|
|||||||
import type { ImageAttachment } from "@/store/app-store";
|
import type { ImageAttachment } from "@/store/app-store";
|
||||||
import {
|
import {
|
||||||
useKeyboardShortcuts,
|
useKeyboardShortcuts,
|
||||||
ACTION_SHORTCUTS,
|
useKeyboardShortcutsConfig,
|
||||||
KeyboardShortcut,
|
KeyboardShortcut,
|
||||||
} from "@/hooks/use-keyboard-shortcuts";
|
} from "@/hooks/use-keyboard-shortcuts";
|
||||||
|
|
||||||
export function AgentView() {
|
export function AgentView() {
|
||||||
const { currentProject, setLastSelectedSession, getLastSelectedSession } = useAppStore();
|
const { currentProject, setLastSelectedSession, getLastSelectedSession } = useAppStore();
|
||||||
|
const shortcuts = useKeyboardShortcutsConfig();
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [selectedImages, setSelectedImages] = useState<ImageAttachment[]>([]);
|
const [selectedImages, setSelectedImages] = useState<ImageAttachment[]>([]);
|
||||||
const [showImageDropZone, setShowImageDropZone] = useState(false);
|
const [showImageDropZone, setShowImageDropZone] = useState(false);
|
||||||
@@ -417,12 +418,12 @@ export function AgentView() {
|
|||||||
|
|
||||||
// Keyboard shortcuts for agent view
|
// Keyboard shortcuts for agent view
|
||||||
const agentShortcuts: KeyboardShortcut[] = useMemo(() => {
|
const agentShortcuts: KeyboardShortcut[] = useMemo(() => {
|
||||||
const shortcuts: KeyboardShortcut[] = [];
|
const shortcutsList: KeyboardShortcut[] = [];
|
||||||
|
|
||||||
// New session shortcut - only when in agent view with a project
|
// New session shortcut - only when in agent view with a project
|
||||||
if (currentProject) {
|
if (currentProject) {
|
||||||
shortcuts.push({
|
shortcutsList.push({
|
||||||
key: ACTION_SHORTCUTS.newSession,
|
key: shortcuts.newSession,
|
||||||
action: () => {
|
action: () => {
|
||||||
if (quickCreateSessionRef.current) {
|
if (quickCreateSessionRef.current) {
|
||||||
quickCreateSessionRef.current();
|
quickCreateSessionRef.current();
|
||||||
@@ -432,8 +433,8 @@ export function AgentView() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return shortcuts;
|
return shortcutsList;
|
||||||
}, [currentProject]);
|
}, [currentProject, shortcuts]);
|
||||||
|
|
||||||
// Register keyboard shortcuts
|
// Register keyboard shortcuts
|
||||||
useKeyboardShortcuts(agentShortcuts);
|
useKeyboardShortcuts(agentShortcuts);
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
useKeyboardShortcuts,
|
useKeyboardShortcuts,
|
||||||
ACTION_SHORTCUTS,
|
useKeyboardShortcutsConfig,
|
||||||
KeyboardShortcut,
|
KeyboardShortcut,
|
||||||
} from "@/hooks/use-keyboard-shortcuts";
|
} from "@/hooks/use-keyboard-shortcuts";
|
||||||
import {
|
import {
|
||||||
@@ -43,6 +43,7 @@ interface ContextFile {
|
|||||||
|
|
||||||
export function ContextView() {
|
export function ContextView() {
|
||||||
const { currentProject } = useAppStore();
|
const { currentProject } = useAppStore();
|
||||||
|
const shortcuts = useKeyboardShortcutsConfig();
|
||||||
const [contextFiles, setContextFiles] = useState<ContextFile[]>([]);
|
const [contextFiles, setContextFiles] = useState<ContextFile[]>([]);
|
||||||
const [selectedFile, setSelectedFile] = useState<ContextFile | null>(null);
|
const [selectedFile, setSelectedFile] = useState<ContextFile | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -63,12 +64,12 @@ export function ContextView() {
|
|||||||
const contextShortcuts: KeyboardShortcut[] = useMemo(
|
const contextShortcuts: KeyboardShortcut[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
key: ACTION_SHORTCUTS.addContextFile,
|
key: shortcuts.addContextFile,
|
||||||
action: () => setIsAddDialogOpen(true),
|
action: () => setIsAddDialogOpen(true),
|
||||||
description: "Add new context file",
|
description: "Add new context file",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[]
|
[shortcuts]
|
||||||
);
|
);
|
||||||
useKeyboardShortcuts(contextShortcuts);
|
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"
|
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"
|
data-testid="shortcut-add-context-file"
|
||||||
>
|
>
|
||||||
{ACTION_SHORTCUTS.addContextFile}
|
{shortcuts.addContextFile}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { cn, modelSupportsThinking } from "@/lib/utils";
|
import { cn, modelSupportsThinking } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
useKeyboardShortcuts,
|
useKeyboardShortcuts,
|
||||||
ACTION_SHORTCUTS,
|
useKeyboardShortcutsConfig,
|
||||||
KeyboardShortcut,
|
KeyboardShortcut,
|
||||||
} from "@/hooks/use-keyboard-shortcuts";
|
} from "@/hooks/use-keyboard-shortcuts";
|
||||||
import {
|
import {
|
||||||
@@ -440,6 +440,7 @@ function ProfileForm({
|
|||||||
export function ProfilesView() {
|
export function ProfilesView() {
|
||||||
const { aiProfiles, addAIProfile, updateAIProfile, removeAIProfile, reorderAIProfiles } =
|
const { aiProfiles, addAIProfile, updateAIProfile, removeAIProfile, reorderAIProfiles } =
|
||||||
useAppStore();
|
useAppStore();
|
||||||
|
const shortcuts = useKeyboardShortcutsConfig();
|
||||||
|
|
||||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||||
const [editingProfile, setEditingProfile] = useState<AIProfile | null>(null);
|
const [editingProfile, setEditingProfile] = useState<AIProfile | null>(null);
|
||||||
@@ -508,17 +509,17 @@ export function ProfilesView() {
|
|||||||
|
|
||||||
// Build keyboard shortcuts for profiles view
|
// Build keyboard shortcuts for profiles view
|
||||||
const profilesShortcuts: KeyboardShortcut[] = useMemo(() => {
|
const profilesShortcuts: KeyboardShortcut[] = useMemo(() => {
|
||||||
const shortcuts: KeyboardShortcut[] = [];
|
const shortcutsList: KeyboardShortcut[] = [];
|
||||||
|
|
||||||
// Add profile shortcut - when in profiles view
|
// Add profile shortcut - when in profiles view
|
||||||
shortcuts.push({
|
shortcutsList.push({
|
||||||
key: ACTION_SHORTCUTS.addProfile,
|
key: shortcuts.addProfile,
|
||||||
action: () => setShowAddDialog(true),
|
action: () => setShowAddDialog(true),
|
||||||
description: "Create new profile",
|
description: "Create new profile",
|
||||||
});
|
});
|
||||||
|
|
||||||
return shortcuts;
|
return shortcutsList;
|
||||||
}, []);
|
}, [shortcuts]);
|
||||||
|
|
||||||
// Register keyboard shortcuts for profiles view
|
// Register keyboard shortcuts for profiles view
|
||||||
useKeyboardShortcuts(profilesShortcuts);
|
useKeyboardShortcuts(profilesShortcuts);
|
||||||
@@ -549,7 +550,7 @@ export function ProfilesView() {
|
|||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
New Profile
|
New Profile
|
||||||
<span className="hidden lg:flex items-center justify-center ml-2 px-2 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/20 border border-primary-foreground/30 text-primary-foreground">
|
<span className="hidden lg:flex items-center justify-center ml-2 px-2 py-0.5 text-[10px] font-mono rounded bg-primary-foreground/20 border border-primary-foreground/30 text-primary-foreground">
|
||||||
{ACTION_SHORTCUTS.addProfile}
|
{shortcuts.addProfile}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useRef, useCallback } from "react";
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -60,6 +61,7 @@ const NAV_ITEMS = [
|
|||||||
{ id: "codex", label: "Codex", icon: Atom },
|
{ id: "codex", label: "Codex", icon: Atom },
|
||||||
{ id: "appearance", label: "Appearance", icon: Palette },
|
{ id: "appearance", label: "Appearance", icon: Palette },
|
||||||
{ id: "kanban", label: "Kanban Display", icon: LayoutGrid },
|
{ id: "kanban", label: "Kanban Display", icon: LayoutGrid },
|
||||||
|
{ id: "keyboard", label: "Keyboard Shortcuts", icon: Settings2 },
|
||||||
{ id: "defaults", label: "Feature Defaults", icon: FlaskConical },
|
{ id: "defaults", label: "Feature Defaults", icon: FlaskConical },
|
||||||
{ id: "danger", label: "Danger Zone", icon: Trash2 },
|
{ id: "danger", label: "Danger Zone", icon: Trash2 },
|
||||||
];
|
];
|
||||||
@@ -81,6 +83,9 @@ export function SettingsView() {
|
|||||||
setShowProfilesOnly,
|
setShowProfilesOnly,
|
||||||
currentProject,
|
currentProject,
|
||||||
moveProjectToTrash,
|
moveProjectToTrash,
|
||||||
|
keyboardShortcuts,
|
||||||
|
setKeyboardShortcut,
|
||||||
|
resetKeyboardShortcuts,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic);
|
const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic);
|
||||||
const [googleKey, setGoogleKey] = useState(apiKeys.google);
|
const [googleKey, setGoogleKey] = useState(apiKeys.google);
|
||||||
@@ -1529,6 +1534,393 @@ export function SettingsView() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Keyboard Shortcuts Section */}
|
||||||
|
<div
|
||||||
|
id="keyboard"
|
||||||
|
className="rounded-xl border border-border bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
|
||||||
|
>
|
||||||
|
<div className="p-6 border-b border-border">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Settings2 className="w-5 h-5 text-brand-500" />
|
||||||
|
<h2 className="text-lg font-semibold text-foreground">
|
||||||
|
Keyboard Shortcuts
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Customize keyboard shortcuts for navigation and actions. Click
|
||||||
|
on any shortcut to edit it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Navigation Shortcuts */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground">
|
||||||
|
Navigation
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => resetKeyboardShortcuts()}
|
||||||
|
className="text-xs h-7"
|
||||||
|
data-testid="reset-shortcuts-button"
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-3 h-3 mr-1" />
|
||||||
|
Reset All to Defaults
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{[
|
||||||
|
{ 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 }) => (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="flex items-center justify-between p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border hover:bg-sidebar-accent/20 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-sm text-foreground">{label}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{editingShortcut === key ? (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
value={shortcutValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
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}`}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
if (!shortcutError && shortcutValue) {
|
||||||
|
setKeyboardShortcut(key, shortcutValue);
|
||||||
|
setEditingShortcut(null);
|
||||||
|
setShortcutValue("");
|
||||||
|
setShortcutError(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!!shortcutError || !shortcutValue}
|
||||||
|
data-testid={`save-shortcut-${key}`}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingShortcut(null);
|
||||||
|
setShortcutValue("");
|
||||||
|
setShortcutError(null);
|
||||||
|
}}
|
||||||
|
data-testid={`cancel-shortcut-${key}`}
|
||||||
|
>
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setEditingShortcut(key);
|
||||||
|
setShortcutValue(keyboardShortcuts[key]);
|
||||||
|
setShortcutError(null);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1.5 text-sm font-mono rounded bg-sidebar-accent/20 border border-sidebar-border hover:bg-sidebar-accent/30 transition-colors",
|
||||||
|
keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] &&
|
||||||
|
"border-brand-500/50 bg-brand-500/10 text-brand-400"
|
||||||
|
)}
|
||||||
|
data-testid={`shortcut-${key}`}
|
||||||
|
>
|
||||||
|
{keyboardShortcuts[key]}
|
||||||
|
</button>
|
||||||
|
{keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] && (
|
||||||
|
<span className="text-xs text-brand-400">(modified)</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{shortcutError && (
|
||||||
|
<p className="text-xs text-red-400">{shortcutError}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* UI Shortcuts */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground">
|
||||||
|
UI Controls
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{[
|
||||||
|
{ key: "toggleSidebar" as keyof KeyboardShortcuts, label: "Toggle Sidebar" },
|
||||||
|
].map(({ key, label }) => (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="flex items-center justify-between p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border hover:bg-sidebar-accent/20 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-sm text-foreground">{label}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{editingShortcut === key ? (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
value={shortcutValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
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}`}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
if (!shortcutError && shortcutValue) {
|
||||||
|
setKeyboardShortcut(key, shortcutValue);
|
||||||
|
setEditingShortcut(null);
|
||||||
|
setShortcutValue("");
|
||||||
|
setShortcutError(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!!shortcutError || !shortcutValue}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingShortcut(null);
|
||||||
|
setShortcutValue("");
|
||||||
|
setShortcutError(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setEditingShortcut(key);
|
||||||
|
setShortcutValue(keyboardShortcuts[key]);
|
||||||
|
setShortcutError(null);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1.5 text-sm font-mono rounded bg-sidebar-accent/20 border border-sidebar-border hover:bg-sidebar-accent/30 transition-colors",
|
||||||
|
keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] &&
|
||||||
|
"border-brand-500/50 bg-brand-500/10 text-brand-400"
|
||||||
|
)}
|
||||||
|
data-testid={`shortcut-${key}`}
|
||||||
|
>
|
||||||
|
{keyboardShortcuts[key]}
|
||||||
|
</button>
|
||||||
|
{keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] && (
|
||||||
|
<span className="text-xs text-brand-400">(modified)</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Shortcuts */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h3 className="text-sm font-semibold text-foreground">
|
||||||
|
Actions
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{[
|
||||||
|
{ 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 }) => (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="flex items-center justify-between p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border hover:bg-sidebar-accent/20 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-sm text-foreground">{label}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{editingShortcut === key ? (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
value={shortcutValue}
|
||||||
|
onChange={(e) => {
|
||||||
|
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}`}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
if (!shortcutError && shortcutValue) {
|
||||||
|
setKeyboardShortcut(key, shortcutValue);
|
||||||
|
setEditingShortcut(null);
|
||||||
|
setShortcutValue("");
|
||||||
|
setShortcutError(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!!shortcutError || !shortcutValue}
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={() => {
|
||||||
|
setEditingShortcut(null);
|
||||||
|
setShortcutValue("");
|
||||||
|
setShortcutError(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setEditingShortcut(key);
|
||||||
|
setShortcutValue(keyboardShortcuts[key]);
|
||||||
|
setShortcutError(null);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
"px-3 py-1.5 text-sm font-mono rounded bg-sidebar-accent/20 border border-sidebar-border hover:bg-sidebar-accent/30 transition-colors",
|
||||||
|
keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] &&
|
||||||
|
"border-brand-500/50 bg-brand-500/10 text-brand-400"
|
||||||
|
)}
|
||||||
|
data-testid={`shortcut-${key}`}
|
||||||
|
>
|
||||||
|
{keyboardShortcuts[key]}
|
||||||
|
</button>
|
||||||
|
{keyboardShortcuts[key] !== DEFAULT_KEYBOARD_SHORTCUTS[key] && (
|
||||||
|
<span className="text-xs text-brand-400">(modified)</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Information */}
|
||||||
|
<div className="flex items-start gap-3 p-4 rounded-lg bg-blue-500/10 border border-blue-500/20">
|
||||||
|
<AlertCircle className="w-5 h-5 text-blue-500 mt-0.5 shrink-0" />
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="font-medium text-blue-400">
|
||||||
|
About Keyboard Shortcuts
|
||||||
|
</p>
|
||||||
|
<p className="text-blue-400/80 text-xs mt-1">
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Feature Defaults Section */}
|
{/* Feature Defaults Section */}
|
||||||
<div
|
<div
|
||||||
id="defaults"
|
id="defaults"
|
||||||
|
|||||||
@@ -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)
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -37,6 +37,60 @@ 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
|
||||||
|
// 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 {
|
export interface ImageAttachment {
|
||||||
id: string;
|
id: string;
|
||||||
data: string; // base64 encoded image data
|
data: string; // base64 encoded image data
|
||||||
@@ -203,6 +257,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 +360,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 +466,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 +970,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 +1071,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,
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user