Merge branch 'v0.9.0rc' into feat/subagents-skills

This commit is contained in:
webdevcody
2026-01-08 00:33:30 -05:00
218 changed files with 18537 additions and 4390 deletions

View File

@@ -0,0 +1,77 @@
import { useState } from 'react';
import { useNavigate } from '@tanstack/react-router';
import { Button } from '@/components/ui/button';
import { LogOut, User } from 'lucide-react';
import { cn } from '@/lib/utils';
import { logout } from '@/lib/http-api-client';
import { useAuthStore } from '@/store/auth-store';
export function AccountSection() {
const navigate = useNavigate();
const [isLoggingOut, setIsLoggingOut] = useState(false);
const handleLogout = async () => {
setIsLoggingOut(true);
try {
await logout();
// Reset auth state
useAuthStore.getState().resetAuth();
// Navigate to logged out page
navigate({ to: '/logged-out' });
} catch (error) {
console.error('Logout failed:', error);
setIsLoggingOut(false);
}
};
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/80 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm'
)}
>
<div className="p-6 border-b border-border/30 bg-gradient-to-r from-primary/5 via-transparent to-transparent">
<div className="flex items-center gap-3 mb-2">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-primary/20 to-primary/10 flex items-center justify-center border border-primary/20">
<User className="w-5 h-5 text-primary" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">Account</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">Manage your session and account.</p>
</div>
<div className="p-6 space-y-4">
{/* Logout */}
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-muted/30 border border-border/30">
<div className="flex items-center gap-3.5 min-w-0">
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-muted/50 to-muted/30 border border-border/30 flex items-center justify-center shrink-0">
<LogOut className="w-5 h-5 text-muted-foreground" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground">Log Out</p>
<p className="text-xs text-muted-foreground/70 mt-0.5">
End your current session and return to the login screen
</p>
</div>
</div>
<Button
variant="outline"
onClick={handleLogout}
disabled={isLoggingOut}
data-testid="logout-button"
className={cn(
'shrink-0 gap-2',
'transition-all duration-200 ease-out',
'hover:scale-[1.02] active:scale-[0.98]'
)}
>
<LogOut className="w-4 h-4" />
{isLoggingOut ? 'Logging out...' : 'Log Out'}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
export { AccountSection } from './account-section';

View File

@@ -1,7 +1,7 @@
import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';
import { Button } from '@/components/ui/button';
import { Key, CheckCircle2, Settings, Trash2, Loader2 } from 'lucide-react';
import { Key, CheckCircle2, Trash2, Loader2 } from 'lucide-react';
import { ApiKeyField } from './api-key-field';
import { buildProviderConfigs } from '@/config/api-providers';
import { SecurityNotice } from './security-notice';
@@ -10,13 +10,13 @@ import { cn } from '@/lib/utils';
import { useState, useCallback } from 'react';
import { getElectronAPI } from '@/lib/electron';
import { toast } from 'sonner';
import { useNavigate } from '@tanstack/react-router';
export function ApiKeysSection() {
const { apiKeys, setApiKeys } = useAppStore();
const { claudeAuthStatus, setClaudeAuthStatus, setSetupComplete } = useSetupStore();
const { claudeAuthStatus, setClaudeAuthStatus, codexAuthStatus, setCodexAuthStatus } =
useSetupStore();
const [isDeletingAnthropicKey, setIsDeletingAnthropicKey] = useState(false);
const navigate = useNavigate();
const [isDeletingOpenaiKey, setIsDeletingOpenaiKey] = useState(false);
const { providerConfigParams, handleSave, saved } = useApiKeyManagement();
@@ -51,11 +51,33 @@ export function ApiKeysSection() {
}
}, [apiKeys, setApiKeys, claudeAuthStatus, setClaudeAuthStatus]);
// Open setup wizard
const openSetupWizard = useCallback(() => {
setSetupComplete(false);
navigate({ to: '/setup' });
}, [setSetupComplete, navigate]);
// Delete OpenAI API key
const deleteOpenaiKey = useCallback(async () => {
setIsDeletingOpenaiKey(true);
try {
const api = getElectronAPI();
if (!api.setup?.deleteApiKey) {
toast.error('Delete API not available');
return;
}
const result = await api.setup.deleteApiKey('openai');
if (result.success) {
setApiKeys({ ...apiKeys, openai: '' });
setCodexAuthStatus({
authenticated: false,
method: 'none',
});
toast.success('OpenAI API key deleted');
} else {
toast.error(result.error || 'Failed to delete API key');
}
} catch (error) {
toast.error('Failed to delete API key');
} finally {
setIsDeletingOpenaiKey(false);
}
}, [apiKeys, setApiKeys, setCodexAuthStatus]);
return (
<div
@@ -111,16 +133,6 @@ export function ApiKeysSection() {
)}
</Button>
<Button
onClick={openSetupWizard}
variant="outline"
className="h-10 border-border"
data-testid="run-setup-wizard"
>
<Settings className="w-4 h-4 mr-2" />
Run Setup Wizard
</Button>
{apiKeys.anthropic && (
<Button
onClick={deleteAnthropicKey}
@@ -137,6 +149,23 @@ export function ApiKeysSection() {
Delete Anthropic Key
</Button>
)}
{apiKeys.openai && (
<Button
onClick={deleteOpenaiKey}
disabled={isDeletingOpenaiKey}
variant="outline"
className="h-10 border-red-500/30 text-red-500 hover:bg-red-500/10 hover:border-red-500/50"
data-testid="delete-openai-key"
>
{isDeletingOpenaiKey ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Trash2 className="w-4 h-4 mr-2" />
)}
Delete OpenAI Key
</Button>
)}
</div>
</div>
</div>

View File

@@ -1,3 +1,4 @@
// @ts-nocheck
import { useState, useEffect } from 'react';
import { createLogger } from '@automaker/utils/logger';
import { useAppStore } from '@/store/app-store';
@@ -14,6 +15,7 @@ interface TestResult {
interface ApiKeyStatus {
hasAnthropicKey: boolean;
hasGoogleKey: boolean;
hasOpenaiKey: boolean;
}
/**
@@ -26,16 +28,20 @@ export function useApiKeyManagement() {
// API key values
const [anthropicKey, setAnthropicKey] = useState(apiKeys.anthropic);
const [googleKey, setGoogleKey] = useState(apiKeys.google);
const [openaiKey, setOpenaiKey] = useState(apiKeys.openai);
// Visibility toggles
const [showAnthropicKey, setShowAnthropicKey] = useState(false);
const [showGoogleKey, setShowGoogleKey] = useState(false);
const [showOpenaiKey, setShowOpenaiKey] = useState(false);
// Test connection states
const [testingConnection, setTestingConnection] = useState(false);
const [testResult, setTestResult] = useState<TestResult | null>(null);
const [testingGeminiConnection, setTestingGeminiConnection] = useState(false);
const [geminiTestResult, setGeminiTestResult] = useState<TestResult | null>(null);
const [testingOpenaiConnection, setTestingOpenaiConnection] = useState(false);
const [openaiTestResult, setOpenaiTestResult] = useState<TestResult | null>(null);
// API key status from environment
const [apiKeyStatus, setApiKeyStatus] = useState<ApiKeyStatus | null>(null);
@@ -47,6 +53,7 @@ export function useApiKeyManagement() {
useEffect(() => {
setAnthropicKey(apiKeys.anthropic);
setGoogleKey(apiKeys.google);
setOpenaiKey(apiKeys.openai);
}, [apiKeys]);
// Check API key status from environment on mount
@@ -60,6 +67,7 @@ export function useApiKeyManagement() {
setApiKeyStatus({
hasAnthropicKey: status.hasAnthropicKey,
hasGoogleKey: status.hasGoogleKey,
hasOpenaiKey: status.hasOpenaiKey,
});
}
} catch (error) {
@@ -135,11 +143,42 @@ export function useApiKeyManagement() {
setTestingGeminiConnection(false);
};
// Test OpenAI/Codex connection
const handleTestOpenaiConnection = async () => {
setTestingOpenaiConnection(true);
setOpenaiTestResult(null);
try {
const api = getElectronAPI();
const data = await api.setup.verifyCodexAuth('api_key', openaiKey);
if (data.success && data.authenticated) {
setOpenaiTestResult({
success: true,
message: 'Connection successful! Codex 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);
}
};
// Save API keys
const handleSave = () => {
setApiKeys({
anthropic: anthropicKey,
google: googleKey,
openai: openaiKey,
});
setSaved(true);
setTimeout(() => setSaved(false), 2000);
@@ -166,6 +205,15 @@ export function useApiKeyManagement() {
onTest: handleTestGeminiConnection,
result: geminiTestResult,
},
openai: {
value: openaiKey,
setValue: setOpenaiKey,
show: showOpenaiKey,
setShow: setShowOpenaiKey,
testing: testingOpenaiConnection,
onTest: handleTestOpenaiConnection,
result: openaiTestResult,
},
};
return {

View File

@@ -1,13 +1,11 @@
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { FileCode, Shield } from 'lucide-react';
import { FileCode } from 'lucide-react';
import { cn } from '@/lib/utils';
interface ClaudeMdSettingsProps {
autoLoadClaudeMd: boolean;
onAutoLoadClaudeMdChange: (enabled: boolean) => void;
enableSandboxMode: boolean;
onEnableSandboxModeChange: (enabled: boolean) => void;
}
/**
@@ -15,23 +13,18 @@ interface ClaudeMdSettingsProps {
*
* UI controls for Claude Agent SDK settings including:
* - Auto-loading of project instructions from .claude/CLAUDE.md files
* - Sandbox mode for isolated bash command execution
*
* Usage:
* ```tsx
* <ClaudeMdSettings
* autoLoadClaudeMd={autoLoadClaudeMd}
* onAutoLoadClaudeMdChange={setAutoLoadClaudeMd}
* enableSandboxMode={enableSandboxMode}
* onEnableSandboxModeChange={setEnableSandboxMode}
* />
* ```
*/
export function ClaudeMdSettings({
autoLoadClaudeMd,
onAutoLoadClaudeMdChange,
enableSandboxMode,
onEnableSandboxModeChange,
}: ClaudeMdSettingsProps) {
return (
<div
@@ -83,32 +76,6 @@ export function ClaudeMdSettings({
</p>
</div>
</div>
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3 mt-2">
<Checkbox
id="enable-sandbox-mode"
checked={enableSandboxMode}
onCheckedChange={(checked) => onEnableSandboxModeChange(checked === true)}
className="mt-1"
data-testid="enable-sandbox-mode-checkbox"
/>
<div className="space-y-1.5">
<Label
htmlFor="enable-sandbox-mode"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<Shield className="w-4 h-4 text-brand-500" />
Enable Sandbox Mode
</Label>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
Run bash commands in an isolated sandbox environment for additional security.
<span className="block mt-1 text-warning/80">
Note: On some systems, enabling sandbox mode may cause the agent to hang without
responding. If you experience issues, try disabling this option.
</span>
</p>
</div>
</div>
</div>
</div>
);

View File

@@ -1,8 +1,9 @@
import { Button } from '@/components/ui/button';
import { Terminal, CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { CliStatus } from '../shared/types';
import type { ClaudeAuthStatus } from '@/store/setup-store';
import { AnthropicIcon } from '@/components/ui/provider-icon';
interface CliStatusProps {
status: CliStatus | null;
@@ -95,7 +96,7 @@ export function ClaudeCliStatus({ status, authStatus, isChecking, onRefresh }: C
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<Terminal className="w-5 h-5 text-brand-500" />
<AnthropicIcon className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">
Claude Code CLI

View File

@@ -0,0 +1,151 @@
import { Button } from '@/components/ui/button';
import { CheckCircle2, AlertCircle, RefreshCw } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { CliStatus } from '../shared/types';
interface CliStatusCardProps {
title: string;
description: string;
status: CliStatus | null;
isChecking: boolean;
onRefresh: () => void;
refreshTestId: string;
icon: React.ComponentType<{ className?: string }>;
fallbackRecommendation: string;
}
export function CliStatusCard({
title,
description,
status,
isChecking,
onRefresh,
refreshTestId,
icon: Icon,
fallbackRecommendation,
}: CliStatusCardProps) {
if (!status) return null;
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<Icon className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">{title}</h2>
</div>
<Button
variant="ghost"
size="icon"
onClick={onRefresh}
disabled={isChecking}
data-testid={refreshTestId}
title={`Refresh ${title} detection`}
className={cn(
'h-9 w-9 rounded-lg',
'hover:bg-accent/50 hover:scale-105',
'transition-all duration-200'
)}
>
<RefreshCw className={cn('w-4 h-4', isChecking && 'animate-spin')} />
</Button>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">{description}</p>
</div>
<div className="p-6 space-y-4">
{status.success && status.status === 'installed' ? (
<div className="space-y-3">
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center border border-emerald-500/20 shrink-0">
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-emerald-400">{title} Installed</p>
<div className="text-xs text-emerald-400/70 mt-1.5 space-y-0.5">
{status.method && (
<p>
Method: <span className="font-mono">{status.method}</span>
</p>
)}
{status.version && (
<p>
Version: <span className="font-mono">{status.version}</span>
</p>
)}
{status.path && (
<p className="truncate" title={status.path}>
Path: <span className="font-mono text-[10px]">{status.path}</span>
</p>
)}
</div>
</div>
</div>
{status.recommendation && (
<p className="text-xs text-muted-foreground/70 ml-1">{status.recommendation}</p>
)}
</div>
) : (
<div className="space-y-4">
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
<div className="w-10 h-10 rounded-xl bg-amber-500/15 flex items-center justify-center border border-amber-500/20 shrink-0 mt-0.5">
<AlertCircle className="w-5 h-5 text-amber-500" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-amber-400">{title} Not Detected</p>
<p className="text-xs text-amber-400/70 mt-1">
{status.recommendation || fallbackRecommendation}
</p>
</div>
</div>
{status.installCommands && (
<div className="space-y-3">
<p className="text-xs font-medium text-foreground/80">Installation Commands:</p>
<div className="space-y-2">
{status.installCommands.npm && (
<div className="p-3 rounded-xl bg-accent/30 border border-border/50">
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">
npm
</p>
<code className="text-xs text-foreground/80 font-mono break-all">
{status.installCommands.npm}
</code>
</div>
)}
{status.installCommands.macos && (
<div className="p-3 rounded-xl bg-accent/30 border border-border/50">
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">
macOS/Linux
</p>
<code className="text-xs text-foreground/80 font-mono break-all">
{status.installCommands.macos}
</code>
</div>
)}
{status.installCommands.windows && (
<div className="p-3 rounded-xl bg-accent/30 border border-border/50">
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">
Windows (PowerShell)
</p>
<code className="text-xs text-foreground/80 font-mono break-all">
{status.installCommands.windows}
</code>
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,237 @@
import { Button } from '@/components/ui/button';
import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { CliStatus } from '../shared/types';
import type { CodexAuthStatus } from '@/store/setup-store';
import { OpenAIIcon } from '@/components/ui/provider-icon';
interface CliStatusProps {
status: CliStatus | null;
authStatus?: CodexAuthStatus | null;
isChecking: boolean;
onRefresh: () => void;
}
function getAuthMethodLabel(method: string): string {
switch (method) {
case 'api_key':
return 'API Key';
case 'api_key_env':
return 'API Key (Environment)';
case 'cli_authenticated':
case 'oauth':
return 'CLI Authentication';
default:
return method || 'Unknown';
}
}
function SkeletonPulse({ className }: { className?: string }) {
return <div className={cn('animate-pulse bg-muted/50 rounded', className)} />;
}
function CodexCliStatusSkeleton() {
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<SkeletonPulse className="w-9 h-9 rounded-xl" />
<SkeletonPulse className="h-6 w-36" />
</div>
<SkeletonPulse className="w-9 h-9 rounded-lg" />
</div>
<div className="ml-12">
<SkeletonPulse className="h-4 w-80" />
</div>
</div>
<div className="p-6 space-y-4">
{/* Installation status skeleton */}
<div className="flex items-center gap-3 p-4 rounded-xl border border-border/30 bg-muted/10">
<SkeletonPulse className="w-10 h-10 rounded-xl" />
<div className="flex-1 space-y-2">
<SkeletonPulse className="h-4 w-40" />
<SkeletonPulse className="h-3 w-32" />
<SkeletonPulse className="h-3 w-48" />
</div>
</div>
{/* Auth status skeleton */}
<div className="flex items-center gap-3 p-4 rounded-xl border border-border/30 bg-muted/10">
<SkeletonPulse className="w-10 h-10 rounded-xl" />
<div className="flex-1 space-y-2">
<SkeletonPulse className="h-4 w-28" />
<SkeletonPulse className="h-3 w-36" />
</div>
</div>
</div>
</div>
);
}
export function CodexCliStatus({ status, authStatus, isChecking, onRefresh }: CliStatusProps) {
if (!status) return <CodexCliStatusSkeleton />;
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<OpenAIIcon className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">Codex CLI</h2>
</div>
<Button
variant="ghost"
size="icon"
onClick={onRefresh}
disabled={isChecking}
data-testid="refresh-codex-cli"
title="Refresh Codex CLI detection"
className={cn(
'h-9 w-9 rounded-lg',
'hover:bg-accent/50 hover:scale-105',
'transition-all duration-200'
)}
>
<RefreshCw className={cn('w-4 h-4', isChecking && 'animate-spin')} />
</Button>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Codex CLI powers OpenAI models for coding and automation workflows.
</p>
</div>
<div className="p-6 space-y-4">
{status.success && status.status === 'installed' ? (
<div className="space-y-3">
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center border border-emerald-500/20 shrink-0">
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-emerald-400">Codex CLI Installed</p>
<div className="text-xs text-emerald-400/70 mt-1.5 space-y-0.5">
{status.method && (
<p>
Method: <span className="font-mono">{status.method}</span>
</p>
)}
{status.version && (
<p>
Version: <span className="font-mono">{status.version}</span>
</p>
)}
{status.path && (
<p className="truncate" title={status.path}>
Path: <span className="font-mono text-[10px]">{status.path}</span>
</p>
)}
</div>
</div>
</div>
{/* Authentication Status */}
{authStatus?.authenticated ? (
<div className="flex items-center gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
<div className="w-10 h-10 rounded-xl bg-emerald-500/15 flex items-center justify-center border border-emerald-500/20 shrink-0">
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-emerald-400">Authenticated</p>
<div className="text-xs text-emerald-400/70 mt-1.5">
<p>
Method:{' '}
<span className="font-mono">{getAuthMethodLabel(authStatus.method)}</span>
</p>
</div>
</div>
</div>
) : (
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
<div className="w-10 h-10 rounded-xl bg-amber-500/15 flex items-center justify-center border border-amber-500/20 shrink-0 mt-0.5">
<XCircle className="w-5 h-5 text-amber-500" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-amber-400">Not Authenticated</p>
<p className="text-xs text-amber-400/70 mt-1">
Run <code className="font-mono bg-amber-500/10 px-1 rounded">codex login</code>{' '}
or set an API key to authenticate.
</p>
</div>
</div>
)}
{status.recommendation && (
<p className="text-xs text-muted-foreground/70 ml-1">{status.recommendation}</p>
)}
</div>
) : (
<div className="space-y-4">
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
<div className="w-10 h-10 rounded-xl bg-amber-500/15 flex items-center justify-center border border-amber-500/20 shrink-0 mt-0.5">
<AlertCircle className="w-5 h-5 text-amber-500" />
</div>
<div className="flex-1">
<p className="text-sm font-medium text-amber-400">Codex CLI Not Detected</p>
<p className="text-xs text-amber-400/70 mt-1">
{status.recommendation ||
'Install Codex CLI to unlock OpenAI models with tool support.'}
</p>
</div>
</div>
{status.installCommands && (
<div className="space-y-3">
<p className="text-xs font-medium text-foreground/80">Installation Commands:</p>
<div className="space-y-2">
{status.installCommands.npm && (
<div className="p-3 rounded-xl bg-accent/30 border border-border/50">
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">
npm
</p>
<code className="text-xs text-foreground/80 font-mono break-all">
{status.installCommands.npm}
</code>
</div>
)}
{status.installCommands.macos && (
<div className="p-3 rounded-xl bg-accent/30 border border-border/50">
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">
macOS/Linux
</p>
<code className="text-xs text-foreground/80 font-mono break-all">
{status.installCommands.macos}
</code>
</div>
)}
{status.installCommands.windows && (
<div className="p-3 rounded-xl bg-accent/30 border border-border/50">
<p className="text-[10px] text-muted-foreground mb-1.5 font-medium uppercase tracking-wider">
Windows (PowerShell)
</p>
<code className="text-xs text-foreground/80 font-mono break-all">
{status.installCommands.windows}
</code>
</div>
)}
</div>
</div>
)}
</div>
)}
</div>
</div>
);
}

View File

@@ -1,6 +1,7 @@
import { Button } from '@/components/ui/button';
import { Terminal, CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
import { CheckCircle2, AlertCircle, RefreshCw, XCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
import { CursorIcon } from '@/components/ui/provider-icon';
interface CursorStatus {
installed: boolean;
@@ -215,7 +216,7 @@ export function CursorCliStatus({ status, isChecking, onRefresh }: CursorCliStat
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<Terminal className="w-5 h-5 text-brand-500" />
<CursorIcon className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">Cursor CLI</h2>
</div>

View File

@@ -0,0 +1,250 @@
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { FileCode, ShieldCheck, Globe, ImageIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import type { CodexApprovalPolicy, CodexSandboxMode } from '@automaker/types';
import { OpenAIIcon } from '@/components/ui/provider-icon';
interface CodexSettingsProps {
autoLoadCodexAgents: boolean;
codexSandboxMode: CodexSandboxMode;
codexApprovalPolicy: CodexApprovalPolicy;
codexEnableWebSearch: boolean;
codexEnableImages: boolean;
onAutoLoadCodexAgentsChange: (enabled: boolean) => void;
onCodexSandboxModeChange: (mode: CodexSandboxMode) => void;
onCodexApprovalPolicyChange: (policy: CodexApprovalPolicy) => void;
onCodexEnableWebSearchChange: (enabled: boolean) => void;
onCodexEnableImagesChange: (enabled: boolean) => void;
}
const CARD_TITLE = 'Codex CLI Settings';
const CARD_SUBTITLE = 'Configure Codex instructions, capabilities, and execution safety defaults.';
const AGENTS_TITLE = 'Auto-load AGENTS.md Instructions';
const AGENTS_DESCRIPTION = 'Automatically inject project instructions from';
const AGENTS_PATH = '.codex/AGENTS.md';
const AGENTS_SUFFIX = 'on each Codex run.';
const WEB_SEARCH_TITLE = 'Enable Web Search';
const WEB_SEARCH_DESCRIPTION =
'Allow Codex to search the web for current information using --search flag.';
const IMAGES_TITLE = 'Enable Image Support';
const IMAGES_DESCRIPTION = 'Allow Codex to process images attached to prompts using -i flag.';
const SANDBOX_TITLE = 'Sandbox Policy';
const APPROVAL_TITLE = 'Approval Policy';
const SANDBOX_SELECT_LABEL = 'Select sandbox policy';
const APPROVAL_SELECT_LABEL = 'Select approval policy';
const SANDBOX_OPTIONS: Array<{
value: CodexSandboxMode;
label: string;
description: string;
}> = [
{
value: 'read-only',
label: 'Read-only',
description: 'Only allow safe, non-mutating commands.',
},
{
value: 'workspace-write',
label: 'Workspace write',
description: 'Allow file edits inside the project workspace.',
},
{
value: 'danger-full-access',
label: 'Full access',
description: 'Allow unrestricted commands (use with care).',
},
];
const APPROVAL_OPTIONS: Array<{
value: CodexApprovalPolicy;
label: string;
description: string;
}> = [
{
value: 'untrusted',
label: 'Untrusted',
description: 'Ask for approval for most commands.',
},
{
value: 'on-failure',
label: 'On failure',
description: 'Ask only if a command fails in the sandbox.',
},
{
value: 'on-request',
label: 'On request',
description: 'Let the agent decide when to ask.',
},
{
value: 'never',
label: 'Never',
description: 'Never ask for approval (least restrictive).',
},
];
export function CodexSettings({
autoLoadCodexAgents,
codexSandboxMode,
codexApprovalPolicy,
codexEnableWebSearch,
codexEnableImages,
onAutoLoadCodexAgentsChange,
onCodexSandboxModeChange,
onCodexApprovalPolicyChange,
onCodexEnableWebSearchChange,
onCodexEnableImagesChange,
}: CodexSettingsProps) {
const sandboxOption = SANDBOX_OPTIONS.find((option) => option.value === codexSandboxMode);
const approvalOption = APPROVAL_OPTIONS.find((option) => option.value === codexApprovalPolicy);
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center gap-3 mb-2">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<OpenAIIcon className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">{CARD_TITLE}</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">{CARD_SUBTITLE}</p>
</div>
<div className="p-6 space-y-5">
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<Checkbox
id="auto-load-codex-agents"
checked={autoLoadCodexAgents}
onCheckedChange={(checked) => onAutoLoadCodexAgentsChange(checked === true)}
className="mt-1"
data-testid="auto-load-codex-agents-checkbox"
/>
<div className="space-y-1.5">
<Label
htmlFor="auto-load-codex-agents"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<FileCode className="w-4 h-4 text-brand-500" />
{AGENTS_TITLE}
</Label>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
{AGENTS_DESCRIPTION}{' '}
<code className="text-[10px] px-1 py-0.5 rounded bg-accent/50">{AGENTS_PATH}</code>{' '}
{AGENTS_SUFFIX}
</p>
</div>
</div>
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<Checkbox
id="codex-enable-web-search"
checked={codexEnableWebSearch}
onCheckedChange={(checked) => onCodexEnableWebSearchChange(checked === true)}
className="mt-1"
data-testid="codex-enable-web-search-checkbox"
/>
<div className="space-y-1.5">
<Label
htmlFor="codex-enable-web-search"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<Globe className="w-4 h-4 text-brand-500" />
{WEB_SEARCH_TITLE}
</Label>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
{WEB_SEARCH_DESCRIPTION}
</p>
</div>
</div>
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<Checkbox
id="codex-enable-images"
checked={codexEnableImages}
onCheckedChange={(checked) => onCodexEnableImagesChange(checked === true)}
className="mt-1"
data-testid="codex-enable-images-checkbox"
/>
<div className="space-y-1.5">
<Label
htmlFor="codex-enable-images"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<ImageIcon className="w-4 h-4 text-brand-500" />
{IMAGES_TITLE}
</Label>
<p className="text-xs text-muted-foreground/80 leading-relaxed">{IMAGES_DESCRIPTION}</p>
</div>
</div>
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<div className="w-10 h-10 mt-0.5 rounded-xl flex items-center justify-center shrink-0 bg-brand-500/10">
<ShieldCheck className="w-5 h-5 text-brand-500" />
</div>
<div className="flex-1 space-y-4">
<div className="flex items-center justify-between gap-4">
<div>
<Label className="text-foreground font-medium">{SANDBOX_TITLE}</Label>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
{sandboxOption?.description}
</p>
</div>
<Select
value={codexSandboxMode}
onValueChange={(value) => onCodexSandboxModeChange(value as CodexSandboxMode)}
>
<SelectTrigger className="w-[180px] h-8" data-testid="codex-sandbox-select">
<SelectValue aria-label={SANDBOX_SELECT_LABEL} />
</SelectTrigger>
<SelectContent>
{SANDBOX_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between gap-4">
<div>
<Label className="text-foreground font-medium">{APPROVAL_TITLE}</Label>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
{approvalOption?.description}
</p>
</div>
<Select
value={codexApprovalPolicy}
onValueChange={(value) => onCodexApprovalPolicyChange(value as CodexApprovalPolicy)}
>
<SelectTrigger className="w-[180px] h-8" data-testid="codex-approval-select">
<SelectValue aria-label={APPROVAL_SELECT_LABEL} />
</SelectTrigger>
<SelectContent>
{APPROVAL_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,238 @@
// @ts-nocheck
import { useCallback, useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import { RefreshCw, AlertCircle } from 'lucide-react';
import { OpenAIIcon } from '@/components/ui/provider-icon';
import { cn } from '@/lib/utils';
import { getElectronAPI } from '@/lib/electron';
import {
formatCodexCredits,
formatCodexPlanType,
formatCodexResetTime,
getCodexWindowLabel,
} from '@/lib/codex-usage-format';
import { useSetupStore } from '@/store/setup-store';
import { useAppStore, type CodexRateLimitWindow } from '@/store/app-store';
const ERROR_NO_API = 'Codex usage API not available';
const CODEX_USAGE_TITLE = 'Codex Usage';
const CODEX_USAGE_SUBTITLE = 'Shows usage limits reported by the Codex CLI.';
const CODEX_AUTH_WARNING = 'Authenticate Codex CLI to view usage limits.';
const CODEX_LOGIN_COMMAND = 'codex login';
const CODEX_NO_USAGE_MESSAGE =
'Usage limits are not available yet. Try refreshing if this persists.';
const UPDATED_LABEL = 'Updated';
const CODEX_FETCH_ERROR = 'Failed to fetch usage';
const CODEX_REFRESH_LABEL = 'Refresh Codex usage';
const PLAN_LABEL = 'Plan';
const CREDITS_LABEL = 'Credits';
const WARNING_THRESHOLD = 75;
const CAUTION_THRESHOLD = 50;
const MAX_PERCENTAGE = 100;
const REFRESH_INTERVAL_MS = 60_000;
const STALE_THRESHOLD_MS = 2 * 60_000;
const USAGE_COLOR_CRITICAL = 'bg-red-500';
const USAGE_COLOR_WARNING = 'bg-amber-500';
const USAGE_COLOR_OK = 'bg-emerald-500';
const isRateLimitWindow = (
limitWindow: CodexRateLimitWindow | null
): limitWindow is CodexRateLimitWindow => Boolean(limitWindow);
export function CodexUsageSection() {
const codexAuthStatus = useSetupStore((state) => state.codexAuthStatus);
const { codexUsage, codexUsageLastUpdated, setCodexUsage } = useAppStore();
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const canFetchUsage = !!codexAuthStatus?.authenticated;
const rateLimits = codexUsage?.rateLimits ?? null;
const primary = rateLimits?.primary ?? null;
const secondary = rateLimits?.secondary ?? null;
const credits = rateLimits?.credits ?? null;
const planType = rateLimits?.planType ?? null;
const rateLimitWindows = [primary, secondary].filter(isRateLimitWindow);
const hasMetrics = rateLimitWindows.length > 0;
const lastUpdatedLabel = codexUsage?.lastUpdated
? new Date(codexUsage.lastUpdated).toLocaleString()
: null;
const showAuthWarning = !canFetchUsage && !codexUsage && !isLoading;
const isStale = !codexUsageLastUpdated || Date.now() - codexUsageLastUpdated > STALE_THRESHOLD_MS;
const fetchUsage = useCallback(async () => {
setIsLoading(true);
setError(null);
try {
const api = getElectronAPI();
if (!api.codex) {
setError(ERROR_NO_API);
return;
}
const result = await api.codex.getUsage();
if ('error' in result) {
setError(result.message || result.error);
return;
}
setCodexUsage(result);
} catch (fetchError) {
const message = fetchError instanceof Error ? fetchError.message : CODEX_FETCH_ERROR;
setError(message);
} finally {
setIsLoading(false);
}
}, [setCodexUsage]);
useEffect(() => {
if (canFetchUsage && isStale) {
void fetchUsage();
}
}, [fetchUsage, canFetchUsage, isStale]);
useEffect(() => {
if (!canFetchUsage) return undefined;
const intervalId = setInterval(() => {
void fetchUsage();
}, REFRESH_INTERVAL_MS);
return () => clearInterval(intervalId);
}, [fetchUsage, canFetchUsage]);
const getUsageColor = (percentage: number) => {
if (percentage >= WARNING_THRESHOLD) {
return USAGE_COLOR_CRITICAL;
}
if (percentage >= CAUTION_THRESHOLD) {
return USAGE_COLOR_WARNING;
}
return USAGE_COLOR_OK;
};
const RateLimitCard = ({
title,
subtitle,
window: limitWindow,
}: {
title: string;
subtitle: string;
window: CodexRateLimitWindow;
}) => {
const safePercentage = Math.min(Math.max(limitWindow.usedPercent, 0), MAX_PERCENTAGE);
const resetLabel = formatCodexResetTime(limitWindow.resetsAt);
return (
<div className="rounded-xl border border-border/60 bg-card/50 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-semibold text-foreground">{title}</p>
<p className="text-xs text-muted-foreground">{subtitle}</p>
</div>
<span className="text-sm font-semibold text-foreground">
{Math.round(safePercentage)}%
</span>
</div>
<div className="mt-3 h-2 w-full rounded-full bg-secondary/60">
<div
className={cn(
'h-full rounded-full transition-all duration-300',
getUsageColor(safePercentage)
)}
style={{ width: `${safePercentage}%` }}
/>
</div>
{resetLabel && <p className="mt-2 text-xs text-muted-foreground">{resetLabel}</p>}
</div>
);
};
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center gap-3 mb-2">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<OpenAIIcon className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">
{CODEX_USAGE_TITLE}
</h2>
<Button
variant="ghost"
size="icon"
onClick={fetchUsage}
disabled={isLoading}
className="ml-auto h-9 w-9 rounded-lg hover:bg-accent/50"
data-testid="refresh-codex-usage"
title={CODEX_REFRESH_LABEL}
>
<RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} />
</Button>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">{CODEX_USAGE_SUBTITLE}</p>
</div>
<div className="p-6 space-y-4">
{showAuthWarning && (
<div className="flex items-start gap-3 p-4 rounded-xl bg-amber-500/10 border border-amber-500/20">
<AlertCircle className="w-5 h-5 text-amber-500 mt-0.5" />
<div className="text-sm text-amber-400">
{CODEX_AUTH_WARNING} Run <span className="font-mono">{CODEX_LOGIN_COMMAND}</span>.
</div>
</div>
)}
{error && (
<div className="flex items-start gap-3 p-4 rounded-xl bg-red-500/10 border border-red-500/20">
<AlertCircle className="w-5 h-5 text-red-500 mt-0.5" />
<div className="text-sm text-red-400">{error}</div>
</div>
)}
{hasMetrics && (
<div className="grid gap-3 sm:grid-cols-2">
{rateLimitWindows.map((limitWindow, index) => {
const { title, subtitle } = getCodexWindowLabel(limitWindow.windowDurationMins);
return (
<RateLimitCard
key={`${title}-${index}`}
title={title}
subtitle={subtitle}
window={limitWindow}
/>
);
})}
</div>
)}
{(planType || credits) && (
<div className="rounded-xl border border-border/60 bg-secondary/20 p-4 text-xs text-muted-foreground">
{planType && (
<div>
{PLAN_LABEL}:{' '}
<span className="text-foreground">{formatCodexPlanType(planType)}</span>
</div>
)}
{credits && (
<div>
{CREDITS_LABEL}:{' '}
<span className="text-foreground">{formatCodexCredits(credits)}</span>
</div>
)}
</div>
)}
{!hasMetrics && !error && canFetchUsage && !isLoading && (
<div className="rounded-xl border border-border/60 bg-secondary/20 p-4 text-xs text-muted-foreground">
{CODEX_NO_USAGE_MESSAGE}
</div>
)}
{lastUpdatedLabel && (
<div className="text-[10px] text-muted-foreground text-right">
{UPDATED_LABEL} {lastUpdatedLabel}
</div>
)}
</div>
</div>
);
}

View File

@@ -1,6 +1,7 @@
import { cn } from '@/lib/utils';
import type { Project } from '@/lib/electron';
import type { NavigationItem } from '../config/navigation';
import { GLOBAL_NAV_ITEMS, PROJECT_NAV_ITEMS } from '../config/navigation';
import type { SettingsViewId } from '../hooks/use-settings-view';
interface SettingsNavigationProps {
@@ -10,33 +11,95 @@ interface SettingsNavigationProps {
onNavigate: (sectionId: SettingsViewId) => void;
}
export function SettingsNavigation({
navItems,
activeSection,
currentProject,
function NavButton({
item,
isActive,
onNavigate,
}: SettingsNavigationProps) {
}: {
item: NavigationItem;
isActive: boolean;
onNavigate: (sectionId: SettingsViewId) => void;
}) {
const Icon = item.icon;
return (
<nav
<button
key={item.id}
onClick={() => onNavigate(item.id)}
className={cn(
'hidden lg:block w-52 shrink-0',
'border-r border-border/50',
'bg-gradient-to-b from-card/80 via-card/60 to-card/40 backdrop-blur-xl'
'group w-full flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200 ease-out text-left relative overflow-hidden',
isActive
? [
'bg-gradient-to-r from-brand-500/15 via-brand-500/10 to-brand-600/5',
'text-foreground',
'border border-brand-500/25',
'shadow-sm shadow-brand-500/5',
]
: [
'text-muted-foreground hover:text-foreground',
'hover:bg-accent/50',
'border border-transparent hover:border-border/40',
],
'hover:scale-[1.01] active:scale-[0.98]'
)}
>
<div className="sticky top-0 p-4 space-y-1.5">
{navItems
.filter((item) => item.id !== 'danger' || currentProject)
.map((item) => {
const Icon = item.icon;
const isActive = activeSection === item.id;
{/* Active indicator bar */}
{isActive && (
<div className="absolute inset-y-0 left-0 w-0.5 bg-gradient-to-b from-brand-400 via-brand-500 to-brand-600 rounded-r-full" />
)}
<Icon
className={cn(
'w-4 h-4 shrink-0 transition-all duration-200',
isActive ? 'text-brand-500' : 'group-hover:text-brand-400 group-hover:scale-110'
)}
/>
<span className="truncate">{item.label}</span>
</button>
);
}
function NavItemWithSubItems({
item,
activeSection,
onNavigate,
}: {
item: NavigationItem;
activeSection: SettingsViewId;
onNavigate: (sectionId: SettingsViewId) => void;
}) {
const hasActiveSubItem = item.subItems?.some((subItem) => subItem.id === activeSection) ?? false;
const isParentActive = item.id === activeSection;
const Icon = item.icon;
return (
<div>
{/* Parent item - non-clickable label */}
<div
className={cn(
'w-full flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium text-muted-foreground',
isParentActive || (hasActiveSubItem && 'text-foreground')
)}
>
<Icon
className={cn(
'w-4 h-4 shrink-0 transition-all duration-200',
isParentActive || hasActiveSubItem ? 'text-brand-500' : 'text-muted-foreground'
)}
/>
<span className="truncate">{item.label}</span>
</div>
{/* Sub-items - always displayed */}
{item.subItems && (
<div className="ml-4 mt-1 space-y-1">
{item.subItems.map((subItem) => {
const SubIcon = subItem.icon;
const isSubActive = subItem.id === activeSection;
return (
<button
key={item.id}
onClick={() => onNavigate(item.id)}
key={subItem.id}
onClick={() => onNavigate(subItem.id)}
className={cn(
'group w-full flex items-center gap-2.5 px-3 py-2.5 rounded-xl text-sm font-medium transition-all duration-200 ease-out text-left relative overflow-hidden',
isActive
'group w-full flex items-center gap-2.5 px-3 py-2 rounded-lg text-sm font-medium transition-all duration-200 ease-out text-left relative overflow-hidden',
isSubActive
? [
'bg-gradient-to-r from-brand-500/15 via-brand-500/10 to-brand-600/5',
'text-foreground',
@@ -52,19 +115,91 @@ export function SettingsNavigation({
)}
>
{/* Active indicator bar */}
{isActive && (
{isSubActive && (
<div className="absolute inset-y-0 left-0 w-0.5 bg-gradient-to-b from-brand-400 via-brand-500 to-brand-600 rounded-r-full" />
)}
<Icon
<SubIcon
className={cn(
'w-4 h-4 shrink-0 transition-all duration-200',
isActive ? 'text-brand-500' : 'group-hover:text-brand-400 group-hover:scale-110'
isSubActive
? 'text-brand-500'
: 'group-hover:text-brand-400 group-hover:scale-110'
)}
/>
<span className="truncate">{item.label}</span>
<span className="truncate">{subItem.label}</span>
</button>
);
})}
</div>
)}
</div>
);
}
export function SettingsNavigation({
activeSection,
currentProject,
onNavigate,
}: SettingsNavigationProps) {
return (
<nav
className={cn(
'hidden lg:block w-52 shrink-0 overflow-y-auto',
'border-r border-border/50',
'bg-gradient-to-b from-card/80 via-card/60 to-card/40 backdrop-blur-xl'
)}
>
<div className="sticky top-0 p-4 space-y-1">
{/* Global Settings Label */}
<div className="px-3 py-2 text-xs font-semibold text-muted-foreground/70 uppercase tracking-wider">
Global Settings
</div>
{/* Global Settings Items */}
<div className="space-y-1">
{GLOBAL_NAV_ITEMS.map((item) =>
item.subItems ? (
<NavItemWithSubItems
key={item.id}
item={item}
activeSection={activeSection}
onNavigate={onNavigate}
/>
) : (
<NavButton
key={item.id}
item={item}
isActive={activeSection === item.id}
onNavigate={onNavigate}
/>
)
)}
</div>
{/* Project Settings - only show when a project is selected */}
{currentProject && (
<>
{/* Divider */}
<div className="my-4 border-t border-border/50" />
{/* Project Settings Label */}
<div className="px-3 py-2 text-xs font-semibold text-muted-foreground/70 uppercase tracking-wider">
Project Settings
</div>
{/* Project Settings Items */}
<div className="space-y-1">
{PROJECT_NAV_ITEMS.map((item) => (
<NavButton
key={item.id}
item={item}
isActive={activeSection === item.id}
onNavigate={onNavigate}
/>
))}
</div>
</>
)}
</div>
</nav>
);

View File

@@ -1,3 +1,4 @@
import React from 'react';
import type { LucideIcon } from 'lucide-react';
import {
Key,
@@ -11,19 +12,37 @@ import {
Workflow,
Plug,
MessageSquareText,
User,
Shield,
} from 'lucide-react';
import { AnthropicIcon, CursorIcon, OpenAIIcon } from '@/components/ui/provider-icon';
import type { SettingsViewId } from '../hooks/use-settings-view';
export interface NavigationItem {
id: SettingsViewId;
label: string;
icon: LucideIcon;
icon: LucideIcon | React.ComponentType<{ className?: string }>;
subItems?: NavigationItem[];
}
// Navigation items for the settings side panel
export const NAV_ITEMS: NavigationItem[] = [
export interface NavigationGroup {
label: string;
items: NavigationItem[];
}
// Global settings - always visible
export const GLOBAL_NAV_ITEMS: NavigationItem[] = [
{ id: 'api-keys', label: 'API Keys', icon: Key },
{ id: 'providers', label: 'AI Providers', icon: Bot },
{
id: 'providers',
label: 'AI Providers',
icon: Bot,
subItems: [
{ id: 'claude-provider', label: 'Claude', icon: AnthropicIcon },
{ id: 'cursor-provider', label: 'Cursor', icon: CursorIcon },
{ id: 'codex-provider', label: 'Codex', icon: OpenAIIcon },
],
},
{ id: 'mcp-servers', label: 'MCP Servers', icon: Plug },
{ id: 'prompts', label: 'Prompt Customization', icon: MessageSquareText },
{ id: 'model-defaults', label: 'Model Defaults', icon: Workflow },
@@ -32,5 +51,14 @@ export const NAV_ITEMS: NavigationItem[] = [
{ id: 'keyboard', label: 'Keyboard Shortcuts', icon: Settings2 },
{ id: 'audio', label: 'Audio', icon: Volume2 },
{ id: 'defaults', label: 'Feature Defaults', icon: FlaskConical },
{ id: 'account', label: 'Account', icon: User },
{ id: 'security', label: 'Security', icon: Shield },
];
// Project-specific settings - only visible when a project is selected
export const PROJECT_NAV_ITEMS: NavigationItem[] = [
{ id: 'danger', label: 'Danger Zone', icon: Trash2 },
];
// Legacy export for backwards compatibility
export const NAV_ITEMS: NavigationItem[] = [...GLOBAL_NAV_ITEMS, ...PROJECT_NAV_ITEMS];

View File

@@ -1,21 +1,14 @@
import { Button } from '@/components/ui/button';
import { Trash2, Folder, AlertTriangle, Shield, RotateCcw } from 'lucide-react';
import { Trash2, Folder, AlertTriangle } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { Project } from '../shared/types';
interface DangerZoneSectionProps {
project: Project | null;
onDeleteClick: () => void;
skipSandboxWarning: boolean;
onResetSandboxWarning: () => void;
}
export function DangerZoneSection({
project,
onDeleteClick,
skipSandboxWarning,
onResetSandboxWarning,
}: DangerZoneSectionProps) {
export function DangerZoneSection({ project, onDeleteClick }: DangerZoneSectionProps) {
return (
<div
className={cn(
@@ -32,43 +25,11 @@ export function DangerZoneSection({
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">Danger Zone</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Destructive actions and reset options.
</p>
<p className="text-sm text-muted-foreground/80 ml-12">Destructive project actions.</p>
</div>
<div className="p-6 space-y-4">
{/* Sandbox Warning Reset */}
{skipSandboxWarning && (
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-destructive/5 border border-destructive/10">
<div className="flex items-center gap-3.5 min-w-0">
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-destructive/15 to-destructive/10 border border-destructive/20 flex items-center justify-center shrink-0">
<Shield className="w-5 h-5 text-destructive" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground">Sandbox Warning Disabled</p>
<p className="text-xs text-muted-foreground/70 mt-0.5">
The sandbox environment warning is hidden on startup
</p>
</div>
</div>
<Button
variant="outline"
onClick={onResetSandboxWarning}
data-testid="reset-sandbox-warning-button"
className={cn(
'shrink-0 gap-2',
'transition-all duration-200 ease-out',
'hover:scale-[1.02] active:scale-[0.98]'
)}
>
<RotateCcw className="w-4 h-4" />
Reset
</Button>
</div>
)}
{/* Project Delete */}
{project && (
{project ? (
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-destructive/5 border border-destructive/10">
<div className="flex items-center gap-3.5 min-w-0">
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500/15 to-brand-600/10 border border-brand-500/20 flex items-center justify-center shrink-0">
@@ -94,13 +55,8 @@ export function DangerZoneSection({
Delete Project
</Button>
</div>
)}
{/* Empty state when nothing to show */}
{!skipSandboxWarning && !project && (
<p className="text-sm text-muted-foreground/60 text-center py-4">
No danger zone actions available.
</p>
) : (
<p className="text-sm text-muted-foreground/60 text-center py-4">No project selected.</p>
)}
</div>
</div>

View File

@@ -12,6 +12,7 @@ import {
ScrollText,
ShieldCheck,
User,
FastForward,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import {
@@ -29,6 +30,7 @@ interface FeatureDefaultsSectionProps {
showProfilesOnly: boolean;
defaultSkipTests: boolean;
enableDependencyBlocking: boolean;
skipVerificationInAutoMode: boolean;
useWorktrees: boolean;
defaultPlanningMode: PlanningMode;
defaultRequirePlanApproval: boolean;
@@ -37,6 +39,7 @@ interface FeatureDefaultsSectionProps {
onShowProfilesOnlyChange: (value: boolean) => void;
onDefaultSkipTestsChange: (value: boolean) => void;
onEnableDependencyBlockingChange: (value: boolean) => void;
onSkipVerificationInAutoModeChange: (value: boolean) => void;
onUseWorktreesChange: (value: boolean) => void;
onDefaultPlanningModeChange: (value: PlanningMode) => void;
onDefaultRequirePlanApprovalChange: (value: boolean) => void;
@@ -47,6 +50,7 @@ export function FeatureDefaultsSection({
showProfilesOnly,
defaultSkipTests,
enableDependencyBlocking,
skipVerificationInAutoMode,
useWorktrees,
defaultPlanningMode,
defaultRequirePlanApproval,
@@ -55,6 +59,7 @@ export function FeatureDefaultsSection({
onShowProfilesOnlyChange,
onDefaultSkipTestsChange,
onEnableDependencyBlockingChange,
onSkipVerificationInAutoModeChange,
onUseWorktreesChange,
onDefaultPlanningModeChange,
onDefaultRequirePlanApprovalChange,
@@ -309,6 +314,34 @@ export function FeatureDefaultsSection({
{/* Separator */}
<div className="border-t border-border/30" />
{/* Skip Verification in Auto Mode Setting */}
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<Checkbox
id="skip-verification-auto-mode"
checked={skipVerificationInAutoMode}
onCheckedChange={(checked) => onSkipVerificationInAutoModeChange(checked === true)}
className="mt-1"
data-testid="skip-verification-auto-mode-checkbox"
/>
<div className="space-y-1.5">
<Label
htmlFor="skip-verification-auto-mode"
className="text-foreground cursor-pointer font-medium flex items-center gap-2"
>
<FastForward className="w-4 h-4 text-brand-500" />
Skip verification in auto mode
</Label>
<p className="text-xs text-muted-foreground/80 leading-relaxed">
When enabled, auto mode will grab features even if their dependencies are not
verified, as long as they are not currently running. This allows faster pipeline
execution without waiting for manual verification.
</p>
</div>
</div>
{/* Separator */}
<div className="border-t border-border/30" />
{/* Worktree Isolation Setting */}
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
<Checkbox

View File

@@ -32,6 +32,53 @@ export function useCliStatus() {
const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false);
// Refresh Claude auth status from the server
const refreshAuthStatus = useCallback(async () => {
const api = getElectronAPI();
if (!api?.setup?.getClaudeStatus) return;
try {
const result = await api.setup.getClaudeStatus();
if (result.success && result.auth) {
// Cast to extended type that includes server-added fields
const auth = result.auth as typeof result.auth & {
oauthTokenValid?: boolean;
apiKeyValid?: boolean;
};
// Map server method names to client method types
// Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, cli_authenticated, none
const validMethods = [
'oauth_token_env',
'oauth_token',
'api_key',
'api_key_env',
'credentials_file',
'cli_authenticated',
'none',
] as const;
type AuthMethod = (typeof validMethods)[number];
const method: AuthMethod = validMethods.includes(auth.method as AuthMethod)
? (auth.method as AuthMethod)
: auth.authenticated
? 'api_key'
: 'none'; // Default authenticated to api_key, not none
const authStatus = {
authenticated: auth.authenticated,
method,
hasCredentialsFile: auth.hasCredentialsFile ?? false,
oauthTokenValid:
auth.oauthTokenValid || auth.hasStoredOAuthToken || auth.hasEnvOAuthToken,
apiKeyValid: auth.apiKeyValid || auth.hasStoredApiKey || auth.hasEnvApiKey,
hasEnvOAuthToken: auth.hasEnvOAuthToken,
hasEnvApiKey: auth.hasEnvApiKey,
};
setClaudeAuthStatus(authStatus);
}
} catch (error) {
logger.error('Failed to refresh Claude auth status:', error);
}
}, [setClaudeAuthStatus]);
// Check CLI status on mount
useEffect(() => {
const checkCliStatus = async () => {
@@ -48,54 +95,13 @@ export function useCliStatus() {
}
// Check Claude auth status (re-fetch on mount to ensure persistence)
if (api?.setup?.getClaudeStatus) {
try {
const result = await api.setup.getClaudeStatus();
if (result.success && result.auth) {
// Cast to extended type that includes server-added fields
const auth = result.auth as typeof result.auth & {
oauthTokenValid?: boolean;
apiKeyValid?: boolean;
};
// Map server method names to client method types
// Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, cli_authenticated, none
const validMethods = [
'oauth_token_env',
'oauth_token',
'api_key',
'api_key_env',
'credentials_file',
'cli_authenticated',
'none',
] as const;
type AuthMethod = (typeof validMethods)[number];
const method: AuthMethod = validMethods.includes(auth.method as AuthMethod)
? (auth.method as AuthMethod)
: auth.authenticated
? 'api_key'
: 'none'; // Default authenticated to api_key, not none
const authStatus = {
authenticated: auth.authenticated,
method,
hasCredentialsFile: auth.hasCredentialsFile ?? false,
oauthTokenValid:
auth.oauthTokenValid || auth.hasStoredOAuthToken || auth.hasEnvOAuthToken,
apiKeyValid: auth.apiKeyValid || auth.hasStoredApiKey || auth.hasEnvApiKey,
hasEnvOAuthToken: auth.hasEnvOAuthToken,
hasEnvApiKey: auth.hasEnvApiKey,
};
setClaudeAuthStatus(authStatus);
}
} catch (error) {
logger.error('Failed to check Claude auth status:', error);
}
}
await refreshAuthStatus();
};
checkCliStatus();
}, [setClaudeAuthStatus]);
}, [refreshAuthStatus]);
// Refresh Claude CLI status
// Refresh Claude CLI status and auth status
const handleRefreshClaudeCli = useCallback(async () => {
setIsCheckingClaudeCli(true);
try {
@@ -104,12 +110,14 @@ export function useCliStatus() {
const status = await api.checkClaudeCli();
setClaudeCliStatus(status);
}
// Also refresh auth status
await refreshAuthStatus();
} catch (error) {
logger.error('Failed to refresh Claude CLI status:', error);
} finally {
setIsCheckingClaudeCli(false);
}
}, []);
}, [refreshAuthStatus]);
return {
claudeCliStatus,

View File

@@ -4,6 +4,9 @@ export type SettingsViewId =
| 'api-keys'
| 'claude'
| 'providers'
| 'claude-provider'
| 'cursor-provider'
| 'codex-provider'
| 'mcp-servers'
| 'prompts'
| 'model-defaults'
@@ -12,6 +15,8 @@ export type SettingsViewId =
| 'keyboard'
| 'audio'
| 'defaults'
| 'account'
| 'security'
| 'danger';
interface UseSettingsViewOptions {

View File

@@ -19,10 +19,12 @@ import {
import {
CLAUDE_MODELS,
CURSOR_MODELS,
CODEX_MODELS,
THINKING_LEVELS,
THINKING_LEVEL_LABELS,
} from '@/components/views/board-view/shared/model-constants';
import { Check, ChevronsUpDown, Star, Brain, Sparkles, ChevronRight } from 'lucide-react';
import { Check, ChevronsUpDown, Star, ChevronRight } from 'lucide-react';
import { AnthropicIcon, CursorIcon, OpenAIIcon } from '@/components/ui/provider-icon';
import { Button } from '@/components/ui/button';
import {
Command,
@@ -140,14 +142,14 @@ export function PhaseModelSelector({
return {
...claudeModel,
label: `${claudeModel.label}${thinkingLabel}`,
icon: Brain,
icon: AnthropicIcon,
};
}
const cursorModel = availableCursorModels.find(
(m) => stripProviderPrefix(m.id) === selectedModel
);
if (cursorModel) return { ...cursorModel, icon: Sparkles };
if (cursorModel) return { ...cursorModel, icon: CursorIcon };
// Check if selectedModel is part of a grouped model
const group = getModelGroup(selectedModel as CursorModelId);
@@ -158,10 +160,14 @@ export function PhaseModelSelector({
label: `${group.label} (${variant?.label || 'Unknown'})`,
description: group.description,
provider: 'cursor' as const,
icon: Sparkles,
icon: CursorIcon,
};
}
// Check Codex models
const codexModel = CODEX_MODELS.find((m) => m.id === selectedModel);
if (codexModel) return { ...codexModel, icon: OpenAIIcon };
return null;
}, [selectedModel, selectedThinkingLevel, availableCursorModels]);
@@ -199,10 +205,11 @@ export function PhaseModelSelector({
}, [availableCursorModels, enabledCursorModels]);
// Group models
const { favorites, claude, cursor } = React.useMemo(() => {
const { favorites, claude, cursor, codex } = React.useMemo(() => {
const favs: typeof CLAUDE_MODELS = [];
const cModels: typeof CLAUDE_MODELS = [];
const curModels: typeof CURSOR_MODELS = [];
const codModels: typeof CODEX_MODELS = [];
// Process Claude Models
CLAUDE_MODELS.forEach((model) => {
@@ -222,9 +229,71 @@ export function PhaseModelSelector({
}
});
return { favorites: favs, claude: cModels, cursor: curModels };
// Process Codex Models
CODEX_MODELS.forEach((model) => {
if (favoriteModels.includes(model.id)) {
favs.push(model);
} else {
codModels.push(model);
}
});
return { favorites: favs, claude: cModels, cursor: curModels, codex: codModels };
}, [favoriteModels, availableCursorModels]);
// Render Codex model item (no thinking level needed)
const renderCodexModelItem = (model: (typeof CODEX_MODELS)[0]) => {
const isSelected = selectedModel === model.id;
const isFavorite = favoriteModels.includes(model.id);
return (
<CommandItem
key={model.id}
value={model.label}
onSelect={() => {
onChange({ model: model.id });
setOpen(false);
}}
className="group flex items-center justify-between py-2"
>
<div className="flex items-center gap-3 overflow-hidden">
<OpenAIIcon
className={cn(
'h-4 w-4 shrink-0',
isSelected ? 'text-primary' : 'text-muted-foreground'
)}
/>
<div className="flex flex-col truncate">
<span className={cn('truncate font-medium', isSelected && 'text-primary')}>
{model.label}
</span>
<span className="truncate text-xs text-muted-foreground">{model.description}</span>
</div>
</div>
<div className="flex items-center gap-1 ml-2">
<Button
variant="ghost"
size="icon"
className={cn(
'h-6 w-6 hover:bg-transparent hover:text-yellow-500 focus:ring-0',
isFavorite
? 'text-yellow-500 opacity-100'
: 'opacity-0 group-hover:opacity-100 text-muted-foreground'
)}
onClick={(e) => {
e.stopPropagation();
toggleFavoriteModel(model.id);
}}
>
<Star className={cn('h-3.5 w-3.5', isFavorite && 'fill-current')} />
</Button>
{isSelected && <Check className="h-4 w-4 text-primary shrink-0" />}
</div>
</CommandItem>
);
};
// Render Cursor model item (no thinking level needed)
const renderCursorModelItem = (model: (typeof CURSOR_MODELS)[0]) => {
const modelValue = stripProviderPrefix(model.id);
@@ -242,7 +311,7 @@ export function PhaseModelSelector({
className="group flex items-center justify-between py-2"
>
<div className="flex items-center gap-3 overflow-hidden">
<Sparkles
<CursorIcon
className={cn(
'h-4 w-4 shrink-0',
isSelected ? 'text-primary' : 'text-muted-foreground'
@@ -311,7 +380,7 @@ export function PhaseModelSelector({
)}
>
<div className="flex items-center gap-3 overflow-hidden">
<Brain
<AnthropicIcon
className={cn(
'h-4 w-4 shrink-0',
isSelected ? 'text-primary' : 'text-muted-foreground'
@@ -358,10 +427,10 @@ export function PhaseModelSelector({
</PopoverTrigger>
<PopoverContent
side="right"
align="center"
avoidCollisions={false}
align="start"
className="w-[220px] p-1"
sideOffset={8}
collisionPadding={16}
onCloseAutoFocus={(e) => e.preventDefault()}
>
<div className="space-y-1">
@@ -445,7 +514,7 @@ export function PhaseModelSelector({
)}
>
<div className="flex items-center gap-3 overflow-hidden">
<Sparkles
<CursorIcon
className={cn(
'h-4 w-4 shrink-0',
groupIsSelected ? 'text-primary' : 'text-muted-foreground'
@@ -474,10 +543,10 @@ export function PhaseModelSelector({
</PopoverTrigger>
<PopoverContent
side="right"
align="center"
avoidCollisions={false}
align="start"
className="w-[220px] p-1"
sideOffset={8}
collisionPadding={16}
onCloseAutoFocus={(e) => e.preventDefault()}
>
<div className="space-y-1">
@@ -603,6 +672,10 @@ export function PhaseModelSelector({
// Standalone Cursor model
return renderCursorModelItem(model);
}
// Codex model
if (model.provider === 'codex') {
return renderCodexModelItem(model);
}
// Claude model
return renderClaudeModelItem(model);
});
@@ -626,6 +699,12 @@ export function PhaseModelSelector({
{standaloneCursorModels.map((model) => renderCursorModelItem(model))}
</CommandGroup>
)}
{codex.length > 0 && (
<CommandGroup heading="Codex Models">
{codex.map((model) => renderCodexModelItem(model))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>

View File

@@ -1,3 +1,4 @@
// @ts-nocheck
import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';
import { useCliStatus } from '../hooks/use-cli-status';

View File

@@ -0,0 +1,183 @@
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Cpu } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { CodexModelId } from '@automaker/types';
import { CODEX_MODEL_MAP } from '@automaker/types';
import { OpenAIIcon } from '@/components/ui/provider-icon';
interface CodexModelConfigurationProps {
enabledCodexModels: CodexModelId[];
codexDefaultModel: CodexModelId;
isSaving: boolean;
onDefaultModelChange: (model: CodexModelId) => void;
onModelToggle: (model: CodexModelId, enabled: boolean) => void;
}
interface CodexModelInfo {
id: CodexModelId;
label: string;
description: string;
}
const CODEX_MODEL_INFO: Record<CodexModelId, CodexModelInfo> = {
'gpt-5.2-codex': {
id: 'gpt-5.2-codex',
label: 'GPT-5.2-Codex',
description: 'Most advanced agentic coding model for complex software engineering',
},
'gpt-5-codex': {
id: 'gpt-5-codex',
label: 'GPT-5-Codex',
description: 'Purpose-built for Codex CLI with versatile tool use',
},
'gpt-5-codex-mini': {
id: 'gpt-5-codex-mini',
label: 'GPT-5-Codex-Mini',
description: 'Faster workflows optimized for low-latency code Q&A and editing',
},
'codex-1': {
id: 'codex-1',
label: 'Codex-1',
description: 'Version of o3 optimized for software engineering',
},
'codex-mini-latest': {
id: 'codex-mini-latest',
label: 'Codex-Mini-Latest',
description: 'Version of o4-mini for Codex, optimized for faster workflows',
},
'gpt-5': {
id: 'gpt-5',
label: 'GPT-5',
description: 'GPT-5 base flagship model',
},
};
export function CodexModelConfiguration({
enabledCodexModels,
codexDefaultModel,
isSaving,
onDefaultModelChange,
onModelToggle,
}: CodexModelConfigurationProps) {
const availableModels = Object.values(CODEX_MODEL_INFO);
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm shadow-black/5'
)}
>
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
<div className="flex items-center gap-3 mb-2">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
<OpenAIIcon className="w-5 h-5 text-brand-500" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">
Model Configuration
</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Configure which Codex models are available in the feature modal
</p>
</div>
<div className="p-6 space-y-6">
<div className="space-y-2">
<Label>Default Model</Label>
<Select
value={codexDefaultModel}
onValueChange={(v) => onDefaultModelChange(v as CodexModelId)}
disabled={isSaving}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{availableModels.map((model) => (
<SelectItem key={model.id} value={model.id}>
<div className="flex items-center gap-2">
<span>{model.label}</span>
{supportsReasoningEffort(model.id) && (
<Badge variant="outline" className="text-xs">
Thinking
</Badge>
)}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-3">
<Label>Available Models</Label>
<div className="grid gap-3">
{availableModels.map((model) => {
const isEnabled = enabledCodexModels.includes(model.id);
const isDefault = model.id === codexDefaultModel;
return (
<div
key={model.id}
className="flex items-center justify-between p-3 rounded-xl border border-border/50 bg-card/50 hover:bg-accent/30 transition-colors"
>
<div className="flex items-center gap-3">
<Checkbox
checked={isEnabled}
onCheckedChange={(checked) => onModelToggle(model.id, !!checked)}
disabled={isSaving || isDefault}
/>
<div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{model.label}</span>
{supportsReasoningEffort(model.id) && (
<Badge variant="outline" className="text-xs">
Thinking
</Badge>
)}
{isDefault && (
<Badge variant="secondary" className="text-xs">
Default
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground">{model.description}</p>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
);
}
function getModelDisplayName(modelId: string): string {
const displayNames: Record<string, string> = {
'gpt-5.2-codex': 'GPT-5.2-Codex',
'gpt-5-codex': 'GPT-5-Codex',
'gpt-5-codex-mini': 'GPT-5-Codex-Mini',
'codex-1': 'Codex-1',
'codex-mini-latest': 'Codex-Mini-Latest',
'gpt-5': 'GPT-5',
};
return displayNames[modelId] || modelId;
}
function supportsReasoningEffort(modelId: string): boolean {
const reasoningModels = ['gpt-5.2-codex', 'gpt-5-codex', 'gpt-5', 'codex-1'];
return reasoningModels.includes(modelId);
}

View File

@@ -0,0 +1,198 @@
import { useState, useCallback, useEffect } from 'react';
import { useAppStore } from '@/store/app-store';
import { useSetupStore } from '@/store/setup-store';
import { CodexCliStatus } from '../cli-status/codex-cli-status';
import { CodexSettings } from '../codex/codex-settings';
import { CodexUsageSection } from '../codex/codex-usage-section';
import { CodexModelConfiguration } from './codex-model-configuration';
import { getElectronAPI } from '@/lib/electron';
import { createLogger } from '@automaker/utils/logger';
import type { CliStatus as SharedCliStatus } from '../shared/types';
import type { CodexModelId } from '@automaker/types';
const logger = createLogger('CodexSettings');
export function CodexSettingsTab() {
const {
codexAutoLoadAgents,
codexSandboxMode,
codexApprovalPolicy,
codexEnableWebSearch,
codexEnableImages,
enabledCodexModels,
codexDefaultModel,
setCodexAutoLoadAgents,
setCodexSandboxMode,
setCodexApprovalPolicy,
setCodexEnableWebSearch,
setCodexEnableImages,
setEnabledCodexModels,
setCodexDefaultModel,
toggleCodexModel,
} = useAppStore();
const {
codexAuthStatus,
codexCliStatus: setupCliStatus,
setCodexCliStatus,
setCodexAuthStatus,
} = useSetupStore();
const [isCheckingCodexCli, setIsCheckingCodexCli] = useState(false);
const [displayCliStatus, setDisplayCliStatus] = useState<SharedCliStatus | null>(null);
const [isSaving, setIsSaving] = useState(false);
const codexCliStatus: SharedCliStatus | null =
displayCliStatus ||
(setupCliStatus
? {
success: true,
status: setupCliStatus.installed ? 'installed' : 'not_installed',
method: setupCliStatus.method,
version: setupCliStatus.version || undefined,
path: setupCliStatus.path || undefined,
}
: null);
// Load Codex CLI status and auth status on mount
useEffect(() => {
const checkCodexStatus = async () => {
const api = getElectronAPI();
if (api?.setup?.getCodexStatus) {
try {
const result = await api.setup.getCodexStatus();
setDisplayCliStatus({
success: result.success,
status: result.installed ? 'installed' : 'not_installed',
method: result.auth?.method,
version: result.version,
path: result.path,
recommendation: result.recommendation,
installCommands: result.installCommands,
});
setCodexCliStatus({
installed: result.installed,
version: result.version,
path: result.path,
method: result.auth?.method || 'none',
});
if (result.auth) {
setCodexAuthStatus({
authenticated: result.auth.authenticated,
method: result.auth.method as
| 'cli_authenticated'
| 'api_key'
| 'api_key_env'
| 'none',
hasAuthFile: result.auth.method === 'cli_authenticated',
hasApiKey: result.auth.hasApiKey,
});
}
} catch (error) {
logger.error('Failed to check Codex CLI status:', error);
}
}
};
checkCodexStatus();
}, [setCodexCliStatus, setCodexAuthStatus]);
const handleRefreshCodexCli = useCallback(async () => {
setIsCheckingCodexCli(true);
try {
const api = getElectronAPI();
if (api?.setup?.getCodexStatus) {
const result = await api.setup.getCodexStatus();
setDisplayCliStatus({
success: result.success,
status: result.installed ? 'installed' : 'not_installed',
method: result.auth?.method,
version: result.version,
path: result.path,
recommendation: result.recommendation,
installCommands: result.installCommands,
});
setCodexCliStatus({
installed: result.installed,
version: result.version,
path: result.path,
method: result.auth?.method || 'none',
});
if (result.auth) {
setCodexAuthStatus({
authenticated: result.auth.authenticated,
method: result.auth.method as 'cli_authenticated' | 'api_key' | 'api_key_env' | 'none',
hasAuthFile: result.auth.method === 'cli_authenticated',
hasApiKey: result.auth.hasApiKey,
});
}
}
} catch (error) {
logger.error('Failed to refresh Codex CLI status:', error);
} finally {
setIsCheckingCodexCli(false);
}
}, [setCodexCliStatus, setCodexAuthStatus]);
const handleDefaultModelChange = useCallback(
(model: CodexModelId) => {
setIsSaving(true);
try {
setCodexDefaultModel(model);
} finally {
setIsSaving(false);
}
},
[setCodexDefaultModel]
);
const handleModelToggle = useCallback(
(model: CodexModelId, enabled: boolean) => {
setIsSaving(true);
try {
toggleCodexModel(model, enabled);
} finally {
setIsSaving(false);
}
},
[toggleCodexModel]
);
const showUsageTracking = codexAuthStatus?.authenticated ?? false;
const authStatusToDisplay = codexAuthStatus;
return (
<div className="space-y-6">
<CodexCliStatus
status={codexCliStatus}
authStatus={authStatusToDisplay}
isChecking={isCheckingCodexCli}
onRefresh={handleRefreshCodexCli}
/>
{showUsageTracking && <CodexUsageSection />}
<CodexModelConfiguration
enabledCodexModels={enabledCodexModels}
codexDefaultModel={codexDefaultModel}
isSaving={isSaving}
onDefaultModelChange={handleDefaultModelChange}
onModelToggle={handleModelToggle}
/>
<CodexSettings
autoLoadCodexAgents={codexAutoLoadAgents}
codexSandboxMode={codexSandboxMode}
codexApprovalPolicy={codexApprovalPolicy}
codexEnableWebSearch={codexEnableWebSearch}
codexEnableImages={codexEnableImages}
onAutoLoadCodexAgentsChange={setCodexAutoLoadAgents}
onCodexSandboxModeChange={setCodexSandboxMode}
onCodexApprovalPolicyChange={setCodexApprovalPolicy}
onCodexEnableWebSearchChange={setCodexEnableWebSearch}
onCodexEnableImagesChange={setCodexEnableImages}
/>
</div>
);
}
export default CodexSettingsTab;

View File

@@ -1,3 +1,4 @@
export { ProviderTabs } from './provider-tabs';
export { ClaudeSettingsTab } from './claude-settings-tab';
export { CursorSettingsTab } from './cursor-settings-tab';
export { CodexSettingsTab } from './codex-settings-tab';

View File

@@ -1,25 +1,30 @@
import React from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Bot, Terminal } from 'lucide-react';
import { AnthropicIcon, CursorIcon, OpenAIIcon } from '@/components/ui/provider-icon';
import { CursorSettingsTab } from './cursor-settings-tab';
import { ClaudeSettingsTab } from './claude-settings-tab';
import { CodexSettingsTab } from './codex-settings-tab';
interface ProviderTabsProps {
defaultTab?: 'claude' | 'cursor';
defaultTab?: 'claude' | 'cursor' | 'codex';
}
export function ProviderTabs({ defaultTab = 'claude' }: ProviderTabsProps) {
return (
<Tabs defaultValue={defaultTab} className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-6">
<TabsList className="grid w-full grid-cols-3 mb-6">
<TabsTrigger value="claude" className="flex items-center gap-2">
<Bot className="w-4 h-4" />
<AnthropicIcon className="w-4 h-4" />
Claude
</TabsTrigger>
<TabsTrigger value="cursor" className="flex items-center gap-2">
<Terminal className="w-4 h-4" />
<CursorIcon className="w-4 h-4" />
Cursor
</TabsTrigger>
<TabsTrigger value="codex" className="flex items-center gap-2">
<OpenAIIcon className="w-4 h-4" />
Codex
</TabsTrigger>
</TabsList>
<TabsContent value="claude">
@@ -29,6 +34,10 @@ export function ProviderTabs({ defaultTab = 'claude' }: ProviderTabsProps) {
<TabsContent value="cursor">
<CursorSettingsTab />
</TabsContent>
<TabsContent value="codex">
<CodexSettingsTab />
</TabsContent>
</Tabs>
);
}

View File

@@ -0,0 +1 @@
export { SecuritySection } from './security-section';

View File

@@ -0,0 +1,71 @@
import { Shield, AlertTriangle } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
interface SecuritySectionProps {
skipSandboxWarning: boolean;
onSkipSandboxWarningChange: (skip: boolean) => void;
}
export function SecuritySection({
skipSandboxWarning,
onSkipSandboxWarningChange,
}: SecuritySectionProps) {
return (
<div
className={cn(
'rounded-2xl overflow-hidden',
'border border-border/50',
'bg-gradient-to-br from-card/80 via-card/70 to-card/80 backdrop-blur-xl',
'shadow-sm'
)}
>
<div className="p-6 border-b border-border/30 bg-gradient-to-r from-primary/5 via-transparent to-transparent">
<div className="flex items-center gap-3 mb-2">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-primary/20 to-primary/10 flex items-center justify-center border border-primary/20">
<Shield className="w-5 h-5 text-primary" />
</div>
<h2 className="text-lg font-semibold text-foreground tracking-tight">Security</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Configure security warnings and protections.
</p>
</div>
<div className="p-6 space-y-4">
{/* Sandbox Warning Toggle */}
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-muted/30 border border-border/30">
<div className="flex items-center gap-3.5 min-w-0">
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-amber-500/15 to-amber-600/10 border border-amber-500/20 flex items-center justify-center shrink-0">
<AlertTriangle className="w-5 h-5 text-amber-500" />
</div>
<div className="min-w-0 flex-1">
<Label
htmlFor="sandbox-warning-toggle"
className="font-medium text-foreground cursor-pointer"
>
Show Sandbox Warning on Startup
</Label>
<p className="text-xs text-muted-foreground/70 mt-0.5">
Display a security warning when not running in a sandboxed environment
</p>
</div>
</div>
<Switch
id="sandbox-warning-toggle"
checked={!skipSandboxWarning}
onCheckedChange={(checked) => onSkipSandboxWarningChange(!checked)}
data-testid="sandbox-warning-toggle"
/>
</div>
{/* Info text */}
<p className="text-xs text-muted-foreground/60 px-4">
When enabled, you&apos;ll see a warning on app startup if you&apos;re not running in a
containerized environment (like Docker). This helps remind you to use proper isolation
when running AI agents.
</p>
</div>
</div>
);
}