mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
Merge branch 'v0.9.0rc' into feat/subagents-skills
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { AccountSection } from './account-section';
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { SecuritySection } from './security-section';
|
||||
@@ -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'll see a warning on app startup if you're not running in a
|
||||
containerized environment (like Docker). This helps remind you to use proper isolation
|
||||
when running AI agents.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user