mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
feat: implement Codex CLI authentication check and integrate with provider
- Added a new utility for checking Codex CLI authentication status using the 'codex login status' command. - Integrated the authentication check into the CodexProvider's installation detection and authentication methods. - Updated Codex CLI status display in the UI to reflect authentication status and method. - Enhanced error handling and logging for better debugging during authentication checks. - Refactored related components to ensure consistent handling of authentication across the application.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LogOut, RefreshCcw } from 'lucide-react';
|
||||
import { LogOut } from 'lucide-react';
|
||||
|
||||
export function LoggedOutView() {
|
||||
const navigate = useNavigate();
|
||||
@@ -22,10 +22,6 @@ export function LoggedOutView() {
|
||||
<Button className="w-full" onClick={() => navigate({ to: '/login' })}>
|
||||
Go to login
|
||||
</Button>
|
||||
<Button className="w-full" variant="secondary" onClick={() => window.location.reload()}>
|
||||
<RefreshCcw className="mr-2 h-4 w-4" />
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
|
||||
import { useSettingsView } from './settings-view/hooks';
|
||||
import { useSettingsView, type SettingsViewId } from './settings-view/hooks';
|
||||
import { NAV_ITEMS } from './settings-view/config/navigation';
|
||||
import { SettingsHeader } from './settings-view/components/settings-header';
|
||||
import { KeyboardMapDialog } from './settings-view/components/keyboard-map-dialog';
|
||||
@@ -18,7 +18,7 @@ import { FeatureDefaultsSection } from './settings-view/feature-defaults/feature
|
||||
import { DangerZoneSection } from './settings-view/danger-zone/danger-zone-section';
|
||||
import { AccountSection } from './settings-view/account';
|
||||
import { SecuritySection } from './settings-view/security';
|
||||
import { ProviderTabs } from './settings-view/providers';
|
||||
import { ClaudeSettingsTab, CursorSettingsTab, CodexSettingsTab } from './settings-view/providers';
|
||||
import { MCPServersSection } from './settings-view/mcp-servers';
|
||||
import { PromptCustomizationSection } from './settings-view/prompts';
|
||||
import type { Project as SettingsProject, Theme } from './settings-view/shared/types';
|
||||
@@ -88,15 +88,30 @@ export function SettingsView() {
|
||||
// Use settings view navigation hook
|
||||
const { activeView, navigateTo } = useSettingsView();
|
||||
|
||||
// Handle navigation - if navigating to 'providers', default to 'claude-provider'
|
||||
const handleNavigate = (viewId: SettingsViewId) => {
|
||||
if (viewId === 'providers') {
|
||||
navigateTo('claude-provider');
|
||||
} else {
|
||||
navigateTo(viewId);
|
||||
}
|
||||
};
|
||||
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
const [showKeyboardMapDialog, setShowKeyboardMapDialog] = useState(false);
|
||||
|
||||
// Render the active section based on current view
|
||||
const renderActiveSection = () => {
|
||||
switch (activeView) {
|
||||
case 'claude-provider':
|
||||
return <ClaudeSettingsTab />;
|
||||
case 'cursor-provider':
|
||||
return <CursorSettingsTab />;
|
||||
case 'codex-provider':
|
||||
return <CodexSettingsTab />;
|
||||
case 'providers':
|
||||
case 'claude': // Backwards compatibility
|
||||
return <ProviderTabs defaultTab={activeView === 'claude' ? 'claude' : undefined} />;
|
||||
case 'claude': // Backwards compatibility - redirect to claude-provider
|
||||
return <ClaudeSettingsTab />;
|
||||
case 'mcp-servers':
|
||||
return <MCPServersSection />;
|
||||
case 'prompts':
|
||||
@@ -181,7 +196,7 @@ export function SettingsView() {
|
||||
navItems={NAV_ITEMS}
|
||||
activeSection={activeView}
|
||||
currentProject={currentProject}
|
||||
onNavigate={navigateTo}
|
||||
onNavigate={handleNavigate}
|
||||
/>
|
||||
|
||||
{/* Content Panel - Shows only the active section */}
|
||||
|
||||
@@ -1,24 +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 { CliStatusCard } from './cli-status-card';
|
||||
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;
|
||||
}
|
||||
|
||||
export function CodexCliStatus({ status, isChecking, onRefresh }: CliStatusProps) {
|
||||
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 (
|
||||
<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."
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,85 @@ function NavButton({
|
||||
);
|
||||
}
|
||||
|
||||
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={subItem.id}
|
||||
onClick={() => onNavigate(subItem.id)}
|
||||
className={cn(
|
||||
'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',
|
||||
'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]'
|
||||
)}
|
||||
>
|
||||
{/* Active indicator bar */}
|
||||
{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" />
|
||||
)}
|
||||
<SubIcon
|
||||
className={cn(
|
||||
'w-4 h-4 shrink-0 transition-all duration-200',
|
||||
isSubActive
|
||||
? 'text-brand-500'
|
||||
: 'group-hover:text-brand-400 group-hover:scale-110'
|
||||
)}
|
||||
/>
|
||||
<span className="truncate">{subItem.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingsNavigation({
|
||||
activeSection,
|
||||
currentProject,
|
||||
@@ -78,14 +157,23 @@ export function SettingsNavigation({
|
||||
|
||||
{/* Global Settings Items */}
|
||||
<div className="space-y-1">
|
||||
{GLOBAL_NAV_ITEMS.map((item) => (
|
||||
<NavButton
|
||||
key={item.id}
|
||||
item={item}
|
||||
isActive={activeSection === item.id}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
))}
|
||||
{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 */}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from 'react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import {
|
||||
Key,
|
||||
@@ -14,12 +15,14 @@ import {
|
||||
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[];
|
||||
}
|
||||
|
||||
export interface NavigationGroup {
|
||||
@@ -30,7 +33,16 @@ export interface NavigationGroup {
|
||||
// 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 },
|
||||
|
||||
@@ -4,6 +4,9 @@ export type SettingsViewId =
|
||||
| 'api-keys'
|
||||
| 'claude'
|
||||
| 'providers'
|
||||
| 'claude-provider'
|
||||
| 'cursor-provider'
|
||||
| 'codex-provider'
|
||||
| 'mcp-servers'
|
||||
| 'prompts'
|
||||
| 'model-defaults'
|
||||
|
||||
@@ -54,7 +54,7 @@ export function CodexSettingsTab() {
|
||||
}
|
||||
: null);
|
||||
|
||||
// Load Codex CLI status on mount
|
||||
// Load Codex CLI status and auth status on mount
|
||||
useEffect(() => {
|
||||
const checkCodexStatus = async () => {
|
||||
const api = getElectronAPI();
|
||||
@@ -158,11 +158,13 @@ export function CodexSettingsTab() {
|
||||
);
|
||||
|
||||
const showUsageTracking = codexAuthStatus?.authenticated ?? false;
|
||||
const authStatusToDisplay = codexAuthStatus;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<CodexCliStatus
|
||||
status={codexCliStatus}
|
||||
authStatus={authStatusToDisplay}
|
||||
isChecking={isCheckingCodexCli}
|
||||
onRefresh={handleRefreshCodexCli}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user