Merge branch 'main' into feature/auth-setup-settings-menu

This commit is contained in:
Ben
2025-12-10 13:37:40 -06:00
committed by GitHub
10 changed files with 664 additions and 93 deletions

View File

@@ -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 && (

View File

@@ -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>
)}

View File

@@ -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<ImageAttachment[]>([]);
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);

View File

@@ -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>
)}

View File

@@ -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<ContextFile[]>([]);
const [selectedFile, setSelectedFile] = useState<ContextFile | null>(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}
</span>
</Button>
</div>

View File

@@ -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<AIProfile | null>(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() {
<Plus className="w-4 h-4 mr-2" />
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">
{ACTION_SHORTCUTS.addProfile}
{shortcuts.addProfile}
</span>
</Button>
</div>

View File

@@ -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() {
</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&apos;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 */}
<div
id="defaults"