"use client"; import { useState, useEffect, useRef, useCallback } from "react"; 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"; import { cn } from "@/lib/utils"; import { Settings, Key, Eye, EyeOff, CheckCircle2, AlertCircle, Loader2, Zap, Sun, Moon, Palette, Terminal, Ghost, Snowflake, Flame, Sparkles, Eclipse, Trees, Cat, Atom, Radio, LayoutGrid, Minimize2, Square, Maximize2, FlaskConical, Trash2, Folder, GitBranch, TestTube, Settings2, RefreshCw, RotateCcw, } from "lucide-react"; import { getElectronAPI } from "@/lib/electron"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; // Navigation items for the side panel const NAV_ITEMS = [ { id: "api-keys", label: "API Keys", icon: Key }, { id: "claude", label: "Claude", icon: Terminal }, { 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 }, ]; export function SettingsView() { const { apiKeys, setApiKeys, setCurrentView, theme, setTheme, kanbanCardDetailLevel, setKanbanCardDetailLevel, defaultSkipTests, setDefaultSkipTests, useWorktrees, setUseWorktrees, showProfilesOnly, setShowProfilesOnly, currentProject, moveProjectToTrash, keyboardShortcuts, setKeyboardShortcut, resetKeyboardShortcuts, } = useAppStore(); const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic); const [googleKey, setGoogleKey] = useState(apiKeys.google); const [openaiKey, setOpenaiKey] = useState(apiKeys.openai); const [showAnthropicKey, setShowAnthropicKey] = useState(false); const [showGoogleKey, setShowGoogleKey] = useState(false); const [showOpenaiKey, setShowOpenaiKey] = useState(false); const [saved, setSaved] = useState(false); const [testingConnection, setTestingConnection] = useState(false); const [testResult, setTestResult] = useState<{ success: boolean; message: string; } | null>(null); const [testingGeminiConnection, setTestingGeminiConnection] = useState(false); const [geminiTestResult, setGeminiTestResult] = useState<{ success: boolean; message: string; } | null>(null); const [claudeCliStatus, setClaudeCliStatus] = useState<{ success: boolean; status?: string; method?: string; version?: string; path?: string; recommendation?: string; installCommands?: { macos?: string; windows?: string; linux?: string; npm?: string; }; error?: string; } | null>(null); const [codexCliStatus, setCodexCliStatus] = useState<{ success: boolean; status?: string; method?: string; version?: string; path?: string; hasApiKey?: boolean; recommendation?: string; installCommands?: { macos?: string; windows?: string; linux?: string; npm?: string; }; error?: string; } | null>(null); const [testingOpenaiConnection, setTestingOpenaiConnection] = useState(false); const [openaiTestResult, setOpenaiTestResult] = useState<{ success: boolean; message: string; } | null>(null); const [activeSection, setActiveSection] = useState("api-keys"); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false); const [isCheckingCodexCli, setIsCheckingCodexCli] = useState(false); const [editingShortcut, setEditingShortcut] = useState(null); const [shortcutValue, setShortcutValue] = useState(""); const [shortcutError, setShortcutError] = useState(null); const scrollContainerRef = useRef(null); useEffect(() => { setAnthropicKey(apiKeys.anthropic); setGoogleKey(apiKeys.google); setOpenaiKey(apiKeys.openai); }, [apiKeys]); useEffect(() => { const checkCliStatus = async () => { const api = getElectronAPI(); if (api?.checkClaudeCli) { try { const status = await api.checkClaudeCli(); setClaudeCliStatus(status); } catch (error) { console.error("Failed to check Claude CLI status:", error); } } if (api?.checkCodexCli) { try { const status = await api.checkCodexCli(); setCodexCliStatus(status); } catch (error) { console.error("Failed to check Codex CLI status:", error); } } }; checkCliStatus(); }, []); // Track scroll position to highlight active nav item useEffect(() => { const container = scrollContainerRef.current; if (!container) return; const handleScroll = () => { const sections = NAV_ITEMS.map((item) => ({ id: item.id, element: document.getElementById(item.id), })).filter((s) => s.element); const containerRect = container.getBoundingClientRect(); const scrollTop = container.scrollTop; for (let i = sections.length - 1; i >= 0; i--) { const section = sections[i]; if (section.element) { const rect = section.element.getBoundingClientRect(); const relativeTop = rect.top - containerRect.top + scrollTop; if (scrollTop >= relativeTop - 100) { setActiveSection(section.id); break; } } } }; container.addEventListener("scroll", handleScroll); return () => container.removeEventListener("scroll", handleScroll); }, []); const scrollToSection = useCallback((sectionId: string) => { const element = document.getElementById(sectionId); if (element && scrollContainerRef.current) { const container = scrollContainerRef.current; const containerRect = container.getBoundingClientRect(); const elementRect = element.getBoundingClientRect(); const relativeTop = elementRect.top - containerRect.top + container.scrollTop; container.scrollTo({ top: relativeTop - 24, behavior: "smooth", }); } }, []); const handleTestConnection = async () => { setTestingConnection(true); setTestResult(null); try { const response = await fetch("/api/claude/test", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ apiKey: anthropicKey }), }); const data = await response.json(); if (response.ok && data.success) { setTestResult({ success: true, message: data.message || "Connection successful! Claude responded.", }); } else { setTestResult({ success: false, message: data.error || "Failed to connect to Claude API.", }); } } catch { setTestResult({ success: false, message: "Network error. Please check your connection.", }); } finally { setTestingConnection(false); } }; const handleTestGeminiConnection = async () => { setTestingGeminiConnection(true); setGeminiTestResult(null); try { const response = await fetch("/api/gemini/test", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ apiKey: googleKey }), }); const data = await response.json(); if (response.ok && data.success) { setGeminiTestResult({ success: true, message: data.message || "Connection successful! Gemini responded.", }); } else { setGeminiTestResult({ success: false, message: data.error || "Failed to connect to Gemini API.", }); } } catch { setGeminiTestResult({ success: false, message: "Network error. Please check your connection.", }); } finally { setTestingGeminiConnection(false); } }; const handleTestOpenaiConnection = async () => { setTestingOpenaiConnection(true); setOpenaiTestResult(null); try { const api = getElectronAPI(); if (api?.testOpenAIConnection) { const result = await api.testOpenAIConnection(openaiKey); if (result.success) { setOpenaiTestResult({ success: true, message: result.message || "Connection successful! OpenAI API responded.", }); } else { setOpenaiTestResult({ success: false, message: result.error || "Failed to connect to OpenAI API.", }); } } else { // Fallback to web API test const response = await fetch("/api/openai/test", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ apiKey: openaiKey }), }); const data = await response.json(); if (response.ok && data.success) { setOpenaiTestResult({ success: true, message: data.message || "Connection successful! OpenAI API responded.", }); } else { setOpenaiTestResult({ success: false, message: data.error || "Failed to connect to OpenAI API.", }); } } } catch { setOpenaiTestResult({ success: false, message: "Network error. Please check your connection.", }); } finally { setTestingOpenaiConnection(false); } }; const handleRefreshClaudeCli = useCallback(async () => { setIsCheckingClaudeCli(true); try { const api = getElectronAPI(); if (api?.checkClaudeCli) { const status = await api.checkClaudeCli(); setClaudeCliStatus(status); } } catch (error) { console.error("Failed to refresh Claude CLI status:", error); } finally { setIsCheckingClaudeCli(false); } }, []); const handleRefreshCodexCli = useCallback(async () => { setIsCheckingCodexCli(true); try { const api = getElectronAPI(); if (api?.checkCodexCli) { const status = await api.checkCodexCli(); setCodexCliStatus(status); } } catch (error) { console.error("Failed to refresh Codex CLI status:", error); } finally { setIsCheckingCodexCli(false); } }, []); const handleSave = () => { setApiKeys({ anthropic: anthropicKey, google: googleKey, openai: openaiKey, }); setSaved(true); setTimeout(() => setSaved(false), 2000); }; return (
{/* Header Section */}

Settings

Configure your API keys and preferences

{/* Content Area with Sidebar */}
{/* Sticky Side Navigation */} {/* Scrollable Content */}
{/* API Keys Section */}

API Keys

Configure your AI provider API keys. Keys are stored locally in your browser.

{/* Claude/Anthropic API Key */}
{apiKeys.anthropic && ( )}
setAnthropicKey(e.target.value)} placeholder="sk-ant-..." className="pr-10 bg-input border-border text-foreground placeholder:text-muted-foreground" data-testid="anthropic-api-key-input" />

Used for Claude AI features. Get your key at{" "} console.anthropic.com . Alternatively, the CLAUDE_CODE_OAUTH_TOKEN environment variable can be used.

{testResult && (
{testResult.success ? ( ) : ( )} {testResult.message}
)}
{/* Google API Key */}
{apiKeys.google && ( )}
setGoogleKey(e.target.value)} placeholder="AIza..." className="pr-10 bg-input border-border text-foreground placeholder:text-muted-foreground" data-testid="google-api-key-input" />

Used for Gemini AI features (including image/design prompts). Get your key at{" "} makersuite.google.com

{geminiTestResult && (
{geminiTestResult.success ? ( ) : ( )} {geminiTestResult.message}
)}
{/* OpenAI API Key */}
{apiKeys.openai && ( )}
setOpenaiKey(e.target.value)} placeholder="sk-..." className="pr-10 bg-input border-border text-foreground placeholder:text-muted-foreground" data-testid="openai-api-key-input" />

Used for OpenAI Codex CLI and GPT models. Get your key at{" "} platform.openai.com

{openaiTestResult && (
{openaiTestResult.success ? ( ) : ( )} {openaiTestResult.message}
)}
{/* Security Notice */}

Security Notice

API keys are stored in your browser's local storage. Never share your API keys or commit them to version control.

{/* Claude CLI Status Section */} {claudeCliStatus && (

Claude Code CLI

Claude Code CLI provides better performance for long-running tasks, especially with ultrathink.

{claudeCliStatus.success && claudeCliStatus.status === "installed" ? (

Claude Code CLI Installed

{claudeCliStatus.method && (

Method:{" "} {claudeCliStatus.method}

)} {claudeCliStatus.version && (

Version:{" "} {claudeCliStatus.version}

)} {claudeCliStatus.path && (

Path:{" "} {claudeCliStatus.path}

)}
{claudeCliStatus.recommendation && (

{claudeCliStatus.recommendation}

)}
) : (

Claude Code CLI Not Detected

{claudeCliStatus.recommendation || "Consider installing Claude Code CLI for optimal performance with ultrathink."}

{claudeCliStatus.installCommands && (

Installation Commands:

{claudeCliStatus.installCommands.npm && (

npm:

{claudeCliStatus.installCommands.npm}
)} {claudeCliStatus.installCommands.macos && (

macOS/Linux:

{claudeCliStatus.installCommands.macos}
)} {claudeCliStatus.installCommands.windows && (

Windows (PowerShell):

{claudeCliStatus.installCommands.windows}
)}
)}
)}
)} {/* Codex CLI Status Section */} {codexCliStatus && (

OpenAI Codex CLI

Codex CLI enables GPT-5.1 Codex models for autonomous coding tasks.

{codexCliStatus.success && codexCliStatus.status === "installed" ? (

Codex CLI Installed

{codexCliStatus.method && (

Method:{" "} {codexCliStatus.method}

)} {codexCliStatus.version && (

Version:{" "} {codexCliStatus.version}

)} {codexCliStatus.path && (

Path:{" "} {codexCliStatus.path}

)}
{codexCliStatus.recommendation && (

{codexCliStatus.recommendation}

)}
) : codexCliStatus.status === "api_key_only" ? (

API Key Detected - CLI Not Installed

{codexCliStatus.recommendation || "OPENAI_API_KEY found but Codex CLI not installed. Install the CLI for full agentic capabilities."}

{codexCliStatus.installCommands && (

Installation Commands:

{codexCliStatus.installCommands.npm && (

npm:

{codexCliStatus.installCommands.npm}
)}
)}
) : (

Codex CLI Not Detected

{codexCliStatus.recommendation || "Install OpenAI Codex CLI to use GPT-5.1 Codex models for autonomous coding."}

{codexCliStatus.installCommands && (

Installation Commands:

{codexCliStatus.installCommands.npm && (

npm:

{codexCliStatus.installCommands.npm}
)} {codexCliStatus.installCommands.macos && (

macOS (Homebrew):

{codexCliStatus.installCommands.macos}
)}
)}
)}
)} {/* Appearance Section */}

Appearance

Customize the look and feel of your application.

{/* Kanban Card Display Section */}

Kanban Card Display

Control how much information is displayed on Kanban cards.

Minimal: Shows only title and category
Standard: Adds steps preview and progress bar
Detailed: Shows all info including model, tool calls, task list, and summaries

{/* 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 */}

Feature Defaults

Configure default settings for new features.

{/* Profiles Only Setting */}
setShowProfilesOnly(checked === true) } className="mt-0.5" data-testid="show-profiles-only-checkbox" />

When enabled, the Add Feature dialog will show only AI profiles and hide advanced model tweaking options (Claude SDK, thinking levels, and OpenAI Codex CLI). This creates a cleaner, less overwhelming UI. You can always disable this to access advanced settings.

{/* Separator */}
{/* Skip Tests Setting */}
setDefaultSkipTests(checked === true) } className="mt-0.5" data-testid="default-skip-tests-checkbox" />

When enabled, new features will default to manual verification instead of TDD (test-driven development). You can still override this for individual features.

{/* Worktree Isolation Setting */}
setUseWorktrees(checked === true) } className="mt-0.5" data-testid="use-worktrees-checkbox" />

Creates isolated git branches for each feature. When disabled, agents work directly in the main project directory. This feature is experimental and may require additional setup like branch selection and merge configuration.

{/* Delete Project Section - Only show when a project is selected */} {currentProject && (

Danger Zone

Permanently remove this project from Automaker.

{currentProject.name}

{currentProject.path}

)} {/* Save Button */}
{/* Delete Project Confirmation Dialog */} Delete Project Are you sure you want to move this project to Trash? {currentProject && (

{currentProject.name}

{currentProject.path}

)}

The folder will remain on disk until you permanently delete it from Trash.

); }