feature/codex-cli

This commit is contained in:
DhanushSantosh
2026-01-06 04:52:25 +05:30
parent 4d4025ca06
commit a57dcc170d
54 changed files with 5562 additions and 91 deletions

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
import type { CliStatus } from '../shared/types';
import { CliStatusCard } from './cli-status-card';
import { OpenAIIcon } from '@/components/ui/provider-icon';
interface CliStatusProps {
status: CliStatus | null;
isChecking: boolean;
onRefresh: () => void;
}
export function CodexCliStatus({ status, isChecking, onRefresh }: CliStatusProps) {
return (
<CliStatusCard
title="Codex CLI"
description="Codex CLI powers OpenAI models for coding and automation workflows."
status={status}
isChecking={isChecking}
onRefresh={onRefresh}
refreshTestId="refresh-codex-cli"
icon={OpenAIIcon}
fallbackRecommendation="Install Codex CLI to unlock OpenAI models with tool support."
/>
);
}

View File

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

View File

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

View File

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

View File

@@ -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'
@@ -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'
@@ -603,6 +672,10 @@ export function PhaseModelSelector({
// Standalone Cursor model
return renderCursorModelItem(model);
}
// Codex model
if (model.provider === 'codex') {
return renderCodexModelItem(model);
}
// Claude model
return renderClaudeModelItem(model);
});
@@ -626,6 +699,12 @@ export function PhaseModelSelector({
{standaloneCursorModels.map((model) => renderCursorModelItem(model))}
</CommandGroup>
)}
{codex.length > 0 && (
<CommandGroup heading="Codex Models">
{codex.map((model) => renderCodexModelItem(model))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>

View File

@@ -0,0 +1,92 @@
import { useState, useCallback } 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 { Info } from 'lucide-react';
import { getElectronAPI } from '@/lib/electron';
import { createLogger } from '@automaker/utils/logger';
const logger = createLogger('CodexSettings');
export function CodexSettingsTab() {
const {
codexAutoLoadAgents,
setCodexAutoLoadAgents,
codexSandboxMode,
setCodexSandboxMode,
codexApprovalPolicy,
setCodexApprovalPolicy,
} = useAppStore();
const { codexAuthStatus, codexCliStatus, setCodexCliStatus, setCodexAuthStatus } =
useSetupStore();
const [isCheckingCodexCli, setIsCheckingCodexCli] = useState(false);
const handleRefreshCodexCli = useCallback(async () => {
setIsCheckingCodexCli(true);
try {
const api = getElectronAPI();
if (api?.setup?.getCodexStatus) {
const result = await api.setup.getCodexStatus();
if (result.success) {
setCodexCliStatus({
installed: result.installed,
version: result.version,
path: result.path,
method: result.method,
});
if (result.auth) {
setCodexAuthStatus({
authenticated: result.auth.authenticated,
method: result.auth.method,
hasAuthFile: result.auth.hasAuthFile,
hasOAuthToken: result.auth.hasOAuthToken,
hasApiKey: result.auth.hasApiKey,
});
}
}
}
} catch (error) {
logger.error('Failed to refresh Codex CLI status:', error);
} finally {
setIsCheckingCodexCli(false);
}
}, [setCodexCliStatus, setCodexAuthStatus]);
// Show usage tracking when CLI is authenticated
const showUsageTracking = codexAuthStatus?.authenticated ?? false;
return (
<div className="space-y-6">
{/* Usage Info */}
<div className="flex items-start gap-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20">
<Info className="w-5 h-5 text-emerald-400 shrink-0 mt-0.5" />
<div className="text-sm text-emerald-400/90">
<span className="font-medium">OpenAI via Codex CLI</span>
<p className="text-xs text-emerald-400/70 mt-1">
Access GPT models with tool support for advanced coding workflows.
</p>
</div>
</div>
<CodexCliStatus
status={codexCliStatus}
isChecking={isCheckingCodexCli}
onRefresh={handleRefreshCodexCli}
/>
<CodexSettings
autoLoadCodexAgents={codexAutoLoadAgents}
codexSandboxMode={codexSandboxMode}
codexApprovalPolicy={codexApprovalPolicy}
onAutoLoadCodexAgentsChange={setCodexAutoLoadAgents}
onCodexSandboxModeChange={setCodexSandboxMode}
onCodexApprovalPolicyChange={setCodexApprovalPolicy}
/>
{showUsageTracking && <CodexUsageSection />}
</div>
);
}
export default CodexSettingsTab;

View File

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

View File

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