Files
automaker/app/src/components/views/settings-view.tsx
2025-12-10 19:32:37 +01:00

1976 lines
86 KiB
TypeScript

"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<string | null>(null);
const [shortcutValue, setShortcutValue] = useState("");
const [shortcutError, setShortcutError] = useState<string | null>(null);
const scrollContainerRef = useRef<HTMLDivElement>(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 (
<div
className="flex-1 flex flex-col overflow-hidden content-bg"
data-testid="settings-view"
>
{/* Header Section */}
<div className="shrink-0 border-b border-border bg-glass backdrop-blur-md">
<div className="px-8 py-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-linear-to-br from-brand-500 to-brand-600 shadow-lg shadow-brand-500/20 flex items-center justify-center">
<Settings className="w-5 h-5 text-primary-foreground" />
</div>
<div>
<h1 className="text-2xl font-bold text-foreground">Settings</h1>
<p className="text-sm text-muted-foreground">
Configure your API keys and preferences
</p>
</div>
</div>
</div>
</div>
{/* Content Area with Sidebar */}
<div className="flex-1 flex overflow-hidden">
{/* Sticky Side Navigation */}
<nav className="hidden lg:block w-48 shrink-0 border-r border-border bg-card/50 backdrop-blur-sm">
<div className="sticky top-0 p-4 space-y-1">
{NAV_ITEMS.filter(
(item) => item.id !== "danger" || currentProject
).map((item) => {
const Icon = item.icon;
const isActive = activeSection === item.id;
return (
<button
key={item.id}
onClick={() => scrollToSection(item.id)}
className={cn(
"w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-all text-left",
isActive
? "bg-brand-500/10 text-brand-500 border border-brand-500/20"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
)}
>
<Icon
className={cn(
"w-4 h-4 shrink-0",
isActive ? "text-brand-500" : ""
)}
/>
<span className="truncate">{item.label}</span>
</button>
);
})}
</div>
</nav>
{/* Scrollable Content */}
<div ref={scrollContainerRef} className="flex-1 overflow-y-auto p-8">
<div className="max-w-4xl mx-auto space-y-6">
{/* API Keys Section */}
<div
id="api-keys"
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">
<Key className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
API Keys
</h2>
</div>
<p className="text-sm text-muted-foreground">
Configure your AI provider API keys. Keys are stored locally
in your browser.
</p>
</div>
<div className="p-6 space-y-6">
{/* Claude/Anthropic API Key */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Label htmlFor="anthropic-key" className="text-foreground">
Anthropic API Key (Claude)
</Label>
{apiKeys.anthropic && (
<CheckCircle2 className="w-4 h-4 text-brand-500" />
)}
</div>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="anthropic-key"
type={showAnthropicKey ? "text" : "password"}
value={anthropicKey}
onChange={(e) => 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"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full px-3 text-muted-foreground hover:text-foreground hover:bg-transparent"
onClick={() => setShowAnthropicKey(!showAnthropicKey)}
data-testid="toggle-anthropic-visibility"
>
{showAnthropicKey ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</Button>
</div>
<Button
type="button"
variant="secondary"
onClick={handleTestConnection}
disabled={!anthropicKey || testingConnection}
className="bg-secondary hover:bg-accent text-secondary-foreground border border-border"
data-testid="test-claude-connection"
>
{testingConnection ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Testing...
</>
) : (
<>
<Zap className="w-4 h-4 mr-2" />
Test
</>
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
Used for Claude AI features. Get your key at{" "}
<a
href="https://console.anthropic.com/account/keys"
target="_blank"
rel="noopener noreferrer"
className="text-brand-500 hover:text-brand-400 hover:underline"
>
console.anthropic.com
</a>
. Alternatively, the CLAUDE_CODE_OAUTH_TOKEN environment
variable can be used.
</p>
{testResult && (
<div
className={`flex items-center gap-2 p-3 rounded-lg ${
testResult.success
? "bg-green-500/10 border border-green-500/20 text-green-400"
: "bg-red-500/10 border border-red-500/20 text-red-400"
}`}
data-testid="test-connection-result"
>
{testResult.success ? (
<CheckCircle2 className="w-4 h-4" />
) : (
<AlertCircle className="w-4 h-4" />
)}
<span
className="text-sm"
data-testid="test-connection-message"
>
{testResult.message}
</span>
</div>
)}
</div>
{/* Google API Key */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Label htmlFor="google-key" className="text-foreground">
Google API Key (Gemini)
</Label>
{apiKeys.google && (
<CheckCircle2 className="w-4 h-4 text-brand-500" />
)}
</div>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="google-key"
type={showGoogleKey ? "text" : "password"}
value={googleKey}
onChange={(e) => 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"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full px-3 text-muted-foreground hover:text-foreground hover:bg-transparent"
onClick={() => setShowGoogleKey(!showGoogleKey)}
data-testid="toggle-google-visibility"
>
{showGoogleKey ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</Button>
</div>
<Button
type="button"
variant="secondary"
onClick={handleTestGeminiConnection}
disabled={!googleKey || testingGeminiConnection}
className="bg-secondary hover:bg-accent text-secondary-foreground border border-border"
data-testid="test-gemini-connection"
>
{testingGeminiConnection ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Testing...
</>
) : (
<>
<Zap className="w-4 h-4 mr-2" />
Test
</>
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
Used for Gemini AI features (including image/design
prompts). Get your key at{" "}
<a
href="https://makersuite.google.com/app/apikey"
target="_blank"
rel="noopener noreferrer"
className="text-brand-500 hover:text-brand-400 hover:underline"
>
makersuite.google.com
</a>
</p>
{geminiTestResult && (
<div
className={`flex items-center gap-2 p-3 rounded-lg ${
geminiTestResult.success
? "bg-green-500/10 border border-green-500/20 text-green-400"
: "bg-red-500/10 border border-red-500/20 text-red-400"
}`}
data-testid="gemini-test-connection-result"
>
{geminiTestResult.success ? (
<CheckCircle2 className="w-4 h-4" />
) : (
<AlertCircle className="w-4 h-4" />
)}
<span
className="text-sm"
data-testid="gemini-test-connection-message"
>
{geminiTestResult.message}
</span>
</div>
)}
</div>
{/* OpenAI API Key */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<Label htmlFor="openai-key" className="text-foreground">
OpenAI API Key (Codex/GPT)
</Label>
{apiKeys.openai && (
<CheckCircle2 className="w-4 h-4 text-brand-500" />
)}
</div>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="openai-key"
type={showOpenaiKey ? "text" : "password"}
value={openaiKey}
onChange={(e) => 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"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full px-3 text-muted-foreground hover:text-foreground hover:bg-transparent"
onClick={() => setShowOpenaiKey(!showOpenaiKey)}
data-testid="toggle-openai-visibility"
>
{showOpenaiKey ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</Button>
</div>
<Button
type="button"
variant="secondary"
onClick={handleTestOpenaiConnection}
disabled={!openaiKey || testingOpenaiConnection}
className="bg-secondary hover:bg-accent text-secondary-foreground border border-border"
data-testid="test-openai-connection"
>
{testingOpenaiConnection ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Testing...
</>
) : (
<>
<Zap className="w-4 h-4 mr-2" />
Test
</>
)}
</Button>
</div>
<p className="text-xs text-muted-foreground">
Used for OpenAI Codex CLI and GPT models. Get your key at{" "}
<a
href="https://platform.openai.com/api-keys"
target="_blank"
rel="noopener noreferrer"
className="text-brand-500 hover:text-brand-400 hover:underline"
>
platform.openai.com
</a>
</p>
{openaiTestResult && (
<div
className={`flex items-center gap-2 p-3 rounded-lg ${
openaiTestResult.success
? "bg-green-500/10 border border-green-500/20 text-green-400"
: "bg-red-500/10 border border-red-500/20 text-red-400"
}`}
data-testid="openai-test-connection-result"
>
{openaiTestResult.success ? (
<CheckCircle2 className="w-4 h-4" />
) : (
<AlertCircle className="w-4 h-4" />
)}
<span
className="text-sm"
data-testid="openai-test-connection-message"
>
{openaiTestResult.message}
</span>
</div>
)}
</div>
{/* Security Notice */}
<div className="flex items-start gap-3 p-4 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
<AlertCircle className="w-5 h-5 text-yellow-500 mt-0.5 shrink-0" />
<div className="text-sm">
<p className="font-medium text-yellow-500">
Security Notice
</p>
<p className="text-yellow-500/80 text-xs mt-1">
API keys are stored in your browser&apos;s local storage.
Never share your API keys or commit them to version
control.
</p>
</div>
</div>
</div>
</div>
{/* Claude CLI Status Section */}
{claudeCliStatus && (
<div
id="claude"
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 justify-between mb-2">
<div className="flex items-center gap-2">
<Terminal className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
Claude Code CLI
</h2>
</div>
<Button
variant="ghost"
size="icon"
onClick={handleRefreshClaudeCli}
disabled={isCheckingClaudeCli}
data-testid="refresh-claude-cli"
title="Refresh Claude CLI detection"
>
<RefreshCw
className={`w-4 h-4 ${
isCheckingClaudeCli ? "animate-spin" : ""
}`}
/>
</Button>
</div>
<p className="text-sm text-muted-foreground">
Claude Code CLI provides better performance for long-running
tasks, especially with ultrathink.
</p>
</div>
<div className="p-6 space-y-4">
{claudeCliStatus.success &&
claudeCliStatus.status === "installed" ? (
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500 shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-green-400">
Claude Code CLI Installed
</p>
<div className="text-xs text-green-400/80 mt-1 space-y-1">
{claudeCliStatus.method && (
<p>
Method:{" "}
<span className="font-mono">
{claudeCliStatus.method}
</span>
</p>
)}
{claudeCliStatus.version && (
<p>
Version:{" "}
<span className="font-mono">
{claudeCliStatus.version}
</span>
</p>
)}
{claudeCliStatus.path && (
<p
className="truncate"
title={claudeCliStatus.path}
>
Path:{" "}
<span className="font-mono text-[10px]">
{claudeCliStatus.path}
</span>
</p>
)}
</div>
</div>
</div>
{claudeCliStatus.recommendation && (
<p className="text-xs text-muted-foreground">
{claudeCliStatus.recommendation}
</p>
)}
</div>
) : (
<div className="space-y-3">
<div className="flex items-start gap-3 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
<AlertCircle className="w-5 h-5 text-yellow-500 mt-0.5 shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-yellow-400">
Claude Code CLI Not Detected
</p>
<p className="text-xs text-yellow-400/80 mt-1">
{claudeCliStatus.recommendation ||
"Consider installing Claude Code CLI for optimal performance with ultrathink."}
</p>
</div>
</div>
{claudeCliStatus.installCommands && (
<div className="space-y-2">
<p className="text-xs font-medium text-foreground-secondary">
Installation Commands:
</p>
<div className="space-y-1">
{claudeCliStatus.installCommands.npm && (
<div className="p-2 rounded bg-background border border-border-glass">
<p className="text-xs text-muted-foreground mb-1">
npm:
</p>
<code className="text-xs text-foreground-secondary font-mono break-all">
{claudeCliStatus.installCommands.npm}
</code>
</div>
)}
{claudeCliStatus.installCommands.macos && (
<div className="p-2 rounded bg-background border border-border-glass">
<p className="text-xs text-muted-foreground mb-1">
macOS/Linux:
</p>
<code className="text-xs text-foreground-secondary font-mono break-all">
{claudeCliStatus.installCommands.macos}
</code>
</div>
)}
{claudeCliStatus.installCommands.windows && (
<div className="p-2 rounded bg-background border border-border-glass">
<p className="text-xs text-muted-foreground mb-1">
Windows (PowerShell):
</p>
<code className="text-xs text-foreground-secondary font-mono break-all">
{claudeCliStatus.installCommands.windows}
</code>
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
)}
{/* Codex CLI Status Section */}
{codexCliStatus && (
<div
id="codex"
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 justify-between mb-2">
<div className="flex items-center gap-2">
<Terminal className="w-5 h-5 text-green-500" />
<h2 className="text-lg font-semibold text-foreground">
OpenAI Codex CLI
</h2>
</div>
<Button
variant="ghost"
size="icon"
onClick={handleRefreshCodexCli}
disabled={isCheckingCodexCli}
data-testid="refresh-codex-cli"
title="Refresh Codex CLI detection"
>
<RefreshCw
className={`w-4 h-4 ${
isCheckingCodexCli ? "animate-spin" : ""
}`}
/>
</Button>
</div>
<p className="text-sm text-muted-foreground">
Codex CLI enables GPT-5.1 Codex models for autonomous coding
tasks.
</p>
</div>
<div className="p-6 space-y-4">
{codexCliStatus.success &&
codexCliStatus.status === "installed" ? (
<div className="space-y-3">
<div className="flex items-center gap-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20">
<CheckCircle2 className="w-5 h-5 text-green-500 shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-green-400">
Codex CLI Installed
</p>
<div className="text-xs text-green-400/80 mt-1 space-y-1">
{codexCliStatus.method && (
<p>
Method:{" "}
<span className="font-mono">
{codexCliStatus.method}
</span>
</p>
)}
{codexCliStatus.version && (
<p>
Version:{" "}
<span className="font-mono">
{codexCliStatus.version}
</span>
</p>
)}
{codexCliStatus.path && (
<p
className="truncate"
title={codexCliStatus.path}
>
Path:{" "}
<span className="font-mono text-[10px]">
{codexCliStatus.path}
</span>
</p>
)}
</div>
</div>
</div>
{codexCliStatus.recommendation && (
<p className="text-xs text-muted-foreground">
{codexCliStatus.recommendation}
</p>
)}
</div>
) : codexCliStatus.status === "api_key_only" ? (
<div className="space-y-3">
<div className="flex items-start gap-3 p-3 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="flex-1">
<p className="text-sm font-medium text-blue-400">
API Key Detected - CLI Not Installed
</p>
<p className="text-xs text-blue-400/80 mt-1">
{codexCliStatus.recommendation ||
"OPENAI_API_KEY found but Codex CLI not installed. Install the CLI for full agentic capabilities."}
</p>
</div>
</div>
{codexCliStatus.installCommands && (
<div className="space-y-2">
<p className="text-xs font-medium text-foreground-secondary">
Installation Commands:
</p>
<div className="space-y-1">
{codexCliStatus.installCommands.npm && (
<div className="p-2 rounded bg-background border border-border-glass">
<p className="text-xs text-muted-foreground mb-1">
npm:
</p>
<code className="text-xs text-foreground-secondary font-mono break-all">
{codexCliStatus.installCommands.npm}
</code>
</div>
)}
</div>
</div>
)}
</div>
) : (
<div className="space-y-3">
<div className="flex items-start gap-3 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/20">
<AlertCircle className="w-5 h-5 text-yellow-500 mt-0.5 shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-yellow-400">
Codex CLI Not Detected
</p>
<p className="text-xs text-yellow-400/80 mt-1">
{codexCliStatus.recommendation ||
"Install OpenAI Codex CLI to use GPT-5.1 Codex models for autonomous coding."}
</p>
</div>
</div>
{codexCliStatus.installCommands && (
<div className="space-y-2">
<p className="text-xs font-medium text-foreground-secondary">
Installation Commands:
</p>
<div className="space-y-1">
{codexCliStatus.installCommands.npm && (
<div className="p-2 rounded bg-background border border-border-glass">
<p className="text-xs text-muted-foreground mb-1">
npm:
</p>
<code className="text-xs text-foreground-secondary font-mono break-all">
{codexCliStatus.installCommands.npm}
</code>
</div>
)}
{codexCliStatus.installCommands.macos && (
<div className="p-2 rounded bg-background border border-border-glass">
<p className="text-xs text-muted-foreground mb-1">
macOS (Homebrew):
</p>
<code className="text-xs text-foreground-secondary font-mono break-all">
{codexCliStatus.installCommands.macos}
</code>
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
)}
{/* Appearance Section */}
<div
id="appearance"
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">
<Palette className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
Appearance
</h2>
</div>
<p className="text-sm text-muted-foreground">
Customize the look and feel of your application.
</p>
</div>
<div className="p-6 space-y-4">
<div className="space-y-3">
<Label className="text-foreground">Theme</Label>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Button
variant={theme === "dark" ? "secondary" : "outline"}
onClick={() => setTheme("dark")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "dark"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="dark-mode-button"
>
<Moon className="w-4 h-4" />
<span className="font-medium text-sm">Dark</span>
</Button>
<Button
variant={theme === "light" ? "secondary" : "outline"}
onClick={() => setTheme("light")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "light"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="light-mode-button"
>
<Sun className="w-4 h-4" />
<span className="font-medium text-sm">Light</span>
</Button>
<Button
variant={theme === "retro" ? "secondary" : "outline"}
onClick={() => setTheme("retro")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "retro"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="retro-mode-button"
>
<Terminal className="w-4 h-4" />
<span className="font-medium text-sm">Retro</span>
</Button>
<Button
variant={theme === "dracula" ? "secondary" : "outline"}
onClick={() => setTheme("dracula")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "dracula"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="dracula-mode-button"
>
<Ghost className="w-4 h-4" />
<span className="font-medium text-sm">Dracula</span>
</Button>
<Button
variant={theme === "nord" ? "secondary" : "outline"}
onClick={() => setTheme("nord")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "nord"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="nord-mode-button"
>
<Snowflake className="w-4 h-4" />
<span className="font-medium text-sm">Nord</span>
</Button>
<Button
variant={theme === "monokai" ? "secondary" : "outline"}
onClick={() => setTheme("monokai")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "monokai"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="monokai-mode-button"
>
<Flame className="w-4 h-4" />
<span className="font-medium text-sm">Monokai</span>
</Button>
<Button
variant={theme === "tokyonight" ? "secondary" : "outline"}
onClick={() => setTheme("tokyonight")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "tokyonight"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="tokyonight-mode-button"
>
<Sparkles className="w-4 h-4" />
<span className="font-medium text-sm">Tokyo Night</span>
</Button>
<Button
variant={theme === "solarized" ? "secondary" : "outline"}
onClick={() => setTheme("solarized")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "solarized"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="solarized-mode-button"
>
<Eclipse className="w-4 h-4" />
<span className="font-medium text-sm">Solarized</span>
</Button>
<Button
variant={theme === "gruvbox" ? "secondary" : "outline"}
onClick={() => setTheme("gruvbox")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "gruvbox"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="gruvbox-mode-button"
>
<Trees className="w-4 h-4" />
<span className="font-medium text-sm">Gruvbox</span>
</Button>
<Button
variant={theme === "catppuccin" ? "secondary" : "outline"}
onClick={() => setTheme("catppuccin")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "catppuccin"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="catppuccin-mode-button"
>
<Cat className="w-4 h-4" />
<span className="font-medium text-sm">Catppuccin</span>
</Button>
<Button
variant={theme === "onedark" ? "secondary" : "outline"}
onClick={() => setTheme("onedark")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "onedark"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="onedark-mode-button"
>
<Atom className="w-4 h-4" />
<span className="font-medium text-sm">One Dark</span>
</Button>
<Button
variant={theme === "synthwave" ? "secondary" : "outline"}
onClick={() => setTheme("synthwave")}
className={`flex items-center justify-center gap-2 px-3 py-3 h-auto ${
theme === "synthwave"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="synthwave-mode-button"
>
<Radio className="w-4 h-4" />
<span className="font-medium text-sm">Synthwave</span>
</Button>
</div>
</div>
</div>
</div>
{/* Kanban Card Display Section */}
<div
id="kanban"
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">
<LayoutGrid className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
Kanban Card Display
</h2>
</div>
<p className="text-sm text-muted-foreground">
Control how much information is displayed on Kanban cards.
</p>
</div>
<div className="p-6 space-y-4">
<div className="space-y-3">
<Label className="text-foreground">Detail Level</Label>
<div className="grid grid-cols-3 gap-3">
<Button
variant={
kanbanCardDetailLevel === "minimal"
? "secondary"
: "outline"
}
onClick={() => setKanbanCardDetailLevel("minimal")}
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 h-auto ${
kanbanCardDetailLevel === "minimal"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="kanban-detail-minimal"
>
<Minimize2 className="w-5 h-5" />
<span className="font-medium text-sm">Minimal</span>
<span className="text-xs text-muted-foreground text-center">
Title & category only
</span>
</Button>
<Button
variant={
kanbanCardDetailLevel === "standard"
? "secondary"
: "outline"
}
onClick={() => setKanbanCardDetailLevel("standard")}
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 h-auto ${
kanbanCardDetailLevel === "standard"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="kanban-detail-standard"
>
<Square className="w-5 h-5" />
<span className="font-medium text-sm">Standard</span>
<span className="text-xs text-muted-foreground text-center">
Steps & progress
</span>
</Button>
<Button
variant={
kanbanCardDetailLevel === "detailed"
? "secondary"
: "outline"
}
onClick={() => setKanbanCardDetailLevel("detailed")}
className={`flex flex-col items-center justify-center gap-2 px-4 py-4 h-auto ${
kanbanCardDetailLevel === "detailed"
? "border-brand-500 ring-1 ring-brand-500/50"
: ""
}`}
data-testid="kanban-detail-detailed"
>
<Maximize2 className="w-5 h-5" />
<span className="font-medium text-sm">Detailed</span>
<span className="text-xs text-muted-foreground text-center">
Model, tools & tasks
</span>
</Button>
</div>
<p className="text-xs text-muted-foreground">
<strong>Minimal:</strong> Shows only title and category
<br />
<strong>Standard:</strong> Adds steps preview and progress
bar
<br />
<strong>Detailed:</strong> Shows all info including model,
tool calls, task list, and summaries
</p>
</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&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"
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">
<FlaskConical className="w-5 h-5 text-brand-500" />
<h2 className="text-lg font-semibold text-foreground">
Feature Defaults
</h2>
</div>
<p className="text-sm text-muted-foreground">
Configure default settings for new features.
</p>
</div>
<div className="p-6 space-y-4">
{/* Profiles Only Setting */}
<div className="space-y-3">
<div className="flex items-start space-x-3">
<Checkbox
id="show-profiles-only"
checked={showProfilesOnly}
onCheckedChange={(checked) =>
setShowProfilesOnly(checked === true)
}
className="mt-0.5"
data-testid="show-profiles-only-checkbox"
/>
<div className="space-y-1">
<Label
htmlFor="show-profiles-only"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<Settings2 className="w-4 h-4 text-brand-500" />
Show profiles only by default
</Label>
<p className="text-xs text-muted-foreground">
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.
</p>
</div>
</div>
</div>
{/* Separator */}
<div className="border-t border-border" />
{/* Skip Tests Setting */}
<div className="space-y-3">
<div className="flex items-start space-x-3">
<Checkbox
id="default-skip-tests"
checked={defaultSkipTests}
onCheckedChange={(checked) =>
setDefaultSkipTests(checked === true)
}
className="mt-0.5"
data-testid="default-skip-tests-checkbox"
/>
<div className="space-y-1">
<Label
htmlFor="default-skip-tests"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<TestTube className="w-4 h-4 text-brand-500" />
Skip automated testing by default
</Label>
<p className="text-xs text-muted-foreground">
When enabled, new features will default to manual
verification instead of TDD (test-driven development).
You can still override this for individual features.
</p>
</div>
</div>
</div>
{/* Worktree Isolation Setting */}
<div className="space-y-3 pt-2 border-t border-border">
<div className="flex items-start space-x-3">
<Checkbox
id="use-worktrees"
checked={useWorktrees}
onCheckedChange={(checked) =>
setUseWorktrees(checked === true)
}
className="mt-0.5"
data-testid="use-worktrees-checkbox"
/>
<div className="space-y-1">
<Label
htmlFor="use-worktrees"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<GitBranch className="w-4 h-4 text-brand-500" />
Enable Git Worktree Isolation (experimental)
</Label>
<p className="text-xs text-muted-foreground">
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.
</p>
</div>
</div>
</div>
</div>
</div>
{/* Delete Project Section - Only show when a project is selected */}
{currentProject && (
<div
id="danger"
className="rounded-xl border border-destructive/30 bg-card backdrop-blur-md overflow-hidden scroll-mt-6"
>
<div className="p-6 border-b border-destructive/30">
<div className="flex items-center gap-2 mb-2">
<Trash2 className="w-5 h-5 text-destructive" />
<h2 className="text-lg font-semibold text-foreground">
Danger Zone
</h2>
</div>
<p className="text-sm text-muted-foreground">
Permanently remove this project from Automaker.
</p>
</div>
<div className="p-6">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3 min-w-0">
<div className="w-10 h-10 rounded-lg bg-sidebar-accent/20 border border-sidebar-border flex items-center justify-center shrink-0">
<Folder className="w-5 h-5 text-brand-500" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground truncate">
{currentProject.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{currentProject.path}
</p>
</div>
</div>
<Button
variant="destructive"
onClick={() => setShowDeleteDialog(true)}
data-testid="delete-project-button"
>
<Trash2 className="w-4 h-4 mr-2" />
Delete Project
</Button>
</div>
</div>
</div>
)}
{/* Save Button */}
<div className="flex items-center gap-4">
<Button
onClick={handleSave}
data-testid="save-settings"
className="min-w-[120px] bg-linear-to-r from-brand-500 to-brand-600 hover:from-brand-600 hover:to-brand-600 text-primary-foreground border-0"
>
{saved ? (
<>
<CheckCircle2 className="w-4 h-4 mr-2" />
Saved!
</>
) : (
"Save Settings"
)}
</Button>
<Button
variant="secondary"
onClick={() => setCurrentView("welcome")}
className="bg-secondary hover:bg-accent text-secondary-foreground border border-border"
data-testid="back-to-home"
>
Back to Home
</Button>
</div>
</div>
</div>
</div>
{/* Delete Project Confirmation Dialog */}
<Dialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
<DialogContent className="bg-popover border-border max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Trash2 className="w-5 h-5 text-destructive" />
Delete Project
</DialogTitle>
<DialogDescription className="text-muted-foreground">
Are you sure you want to move this project to Trash?
</DialogDescription>
</DialogHeader>
{currentProject && (
<div className="flex items-center gap-3 p-4 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
<div className="w-10 h-10 rounded-lg bg-sidebar-accent/20 border border-sidebar-border flex items-center justify-center shrink-0">
<Folder className="w-5 h-5 text-brand-500" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground truncate">
{currentProject.name}
</p>
<p className="text-xs text-muted-foreground truncate">
{currentProject.path}
</p>
</div>
</div>
)}
<p className="text-sm text-muted-foreground">
The folder will remain on disk until you permanently delete it from
Trash.
</p>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="ghost" onClick={() => setShowDeleteDialog(false)}>
Cancel
</Button>
<Button
variant="destructive"
onClick={() => {
if (currentProject) {
moveProjectToTrash(currentProject.id);
setShowDeleteDialog(false);
}
}}
data-testid="confirm-delete-project"
>
<Trash2 className="w-4 h-4 mr-2" />
Move to Trash
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}