"use client"; import { useState, useEffect } from "react"; import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components"; import CliStatusBadge from "./CliStatusBadge"; import { useTranslations } from "next-intl"; import ProviderIcon from "@/shared/components/ProviderIcon"; import { normalizeCodexBaseUrl } from "@/shared/utils/codexBaseUrl"; import { isApplyDisabled, isResetDisabled } from "./codexButtonState"; export default function CodexToolCard({ tool, isExpanded = false, onToggle = () => {}, baseUrl, apiKeys, activeProviders, cloudEnabled, batchStatus, lastConfiguredAt, }) { const t = useTranslations("cliTools"); const [codexStatus, setCodexStatus] = useState(null); const [checkingCodex, setCheckingCodex] = useState(false); const [applying, setApplying] = useState(false); const [restoring, setRestoring] = useState(false); const [message, setMessage] = useState(null); const [showInstallGuide, setShowInstallGuide] = useState(false); const [selectedApiKey, setSelectedApiKey] = useState(""); const [selectedModel, setSelectedModel] = useState("gpt-5.5"); const CODEX_DEFAULT_MODELS = [ "gpt-5.5", "gpt-5.3-codex", "gpt-5.4", "gpt-5.1-codex-max", "gpt-5.1-codex-mini", ]; const [modelMappings, setModelMappings] = useState>({}); const [reasoningEffort, setReasoningEffort] = useState("xhigh"); const [wireApi, setWireApi] = useState("chat"); const [modalOpen, setModalOpen] = useState(false); const [modalTarget, setModalTarget] = useState(null); // null = default model, string = mapping key const [modelAliases, setModelAliases] = useState({}); const [showManualConfigModal, setShowManualConfigModal] = useState(false); const [customBaseUrl, setCustomBaseUrl] = useState(""); // Profiles state const [profiles, setProfiles] = useState([]); const [showProfiles, setShowProfiles] = useState(false); const [newProfileName, setNewProfileName] = useState(""); const [savingProfile, setSavingProfile] = useState(false); const [activatingProfile, setActivatingProfile] = useState(null); // Backups state const [backups, setBackups] = useState([]); const [showBackups, setShowBackups] = useState(false); const [restoringBackup, setRestoringBackup] = useState(null); const cliReady = !!(codexStatus?.installed && codexStatus?.runnable); useEffect(() => { // Store the key *id* so the backend can resolve the real secret from DB if (apiKeys?.length > 0 && !selectedApiKey) { setSelectedApiKey(apiKeys[0].id); } }, [apiKeys, selectedApiKey]); useEffect(() => { if (isExpanded && !codexStatus) { checkCodexStatus(); fetchModelAliases(); fetchProfiles(); fetchBackups(); } }, [isExpanded, codexStatus]); const fetchModelAliases = async () => { try { const res = await fetch("/api/models/alias"); const data = await res.json(); if (res.ok) setModelAliases(data.aliases || {}); } catch (error) { console.log("Error fetching model aliases:", error); } }; // Parse config content useEffect(() => { if (codexStatus?.config) { const modelMatch = codexStatus.config.match(/^model\s*=\s*"([^"]+)"/im); if (modelMatch) setSelectedModel(modelMatch[1]); const effortMatch = codexStatus.config.match(/^model_reasoning_effort\s*=\s*"([^"]+)"/im); if (effortMatch) setReasoningEffort(effortMatch[1]); const wireMatch = codexStatus.config.match(/^wire_api\s*=\s*"([^"]+)"/im); if (wireMatch) setWireApi(wireMatch[1]); const newMappings: Record = {}; const migrationsBlock = codexStatus.config.split("[notice.model_migrations]")[1]; if (migrationsBlock) { const nextSectionIdx = migrationsBlock.indexOf("["); const chunk = nextSectionIdx === -1 ? migrationsBlock : migrationsBlock.substring(0, nextSectionIdx); const lines = chunk.split("\n"); for (const line of lines) { const match = line.match(/^"([^"]+)"\s*=\s*"([^"]+)"/); if (match) newMappings[match[1]] = match[2]; } } setModelMappings(newMappings); } }, [codexStatus]); const getConfigStatus = () => { if (!cliReady) return null; if (!codexStatus.config) return "not_configured"; const hasBaseUrl = codexStatus.config.includes(baseUrl) || codexStatus.config.includes("localhost") || codexStatus.config.includes("127.0.0.1"); return hasBaseUrl ? "configured" : "other"; }; const configStatus = getConfigStatus(); // Use batch status as fallback when card hasn't been expanded yet const effectiveConfigStatus = configStatus || batchStatus?.configStatus || null; const getEffectiveBaseUrl = () => normalizeCodexBaseUrl(customBaseUrl || baseUrl, wireApi); const getDisplayUrl = () => normalizeCodexBaseUrl(customBaseUrl || baseUrl, wireApi); const checkCodexStatus = async () => { setCheckingCodex(true); try { const res = await fetch("/api/cli-tools/codex-settings"); const data = await res.json(); setCodexStatus(data); } catch (error) { setCodexStatus({ installed: false, error: error.message }); } finally { setCheckingCodex(false); } }; const handleApplySettings = async () => { setApplying(true); setMessage(null); try { // Use sk_omniroute for localhost if no key, otherwise use selected key const keyToUse = selectedApiKey && selectedApiKey.trim() ? selectedApiKey : !cloudEnabled ? "sk_omniroute" : selectedApiKey; // Send both apiKey (as fallback) and keyId to look up the unmasked string natively const res = await fetch("/api/cli-tools/codex-settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ baseUrl: getEffectiveBaseUrl(), apiKey: keyToUse, keyId: selectedApiKey, model: selectedModel || CODEX_DEFAULT_MODELS[0], reasoningEffort, wireApi, modelMappings, }), }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: t("settingsApplied") }); checkCodexStatus(); } else { setMessage({ type: "error", text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failedApplySettings"), }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setApplying(false); } }; const handleResetSettings = async () => { setRestoring(true); setMessage(null); try { const res = await fetch("/api/cli-tools/codex-settings", { method: "DELETE" }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: t("settingsReset") }); setSelectedModel(""); checkCodexStatus(); } else { setMessage({ type: "error", text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failedResetSettings"), }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setRestoring(false); } }; const handleModelSelect = (model) => { if (modalTarget) { // Writing to a model mapping alias setModelMappings({ ...modelMappings, [modalTarget]: model.value }); } else { // Writing to the default model setSelectedModel(model.value); } setModalOpen(false); setModalTarget(null); }; // ── Profiles ── const fetchProfiles = async () => { try { const res = await fetch("/api/cli-tools/codex-profiles"); const data = await res.json(); if (res.ok) setProfiles(data.profiles || []); } catch (error) { console.log("Error fetching profiles:", error); } }; const handleSaveProfile = async () => { if (!newProfileName.trim()) return; setSavingProfile(true); setMessage(null); try { const res = await fetch("/api/cli-tools/codex-profiles", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: newProfileName.trim() }), }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: t("profileSaved", { name: newProfileName }) }); setNewProfileName(""); fetchProfiles(); } else { setMessage({ type: "error", text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failedSaveProfile"), }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setSavingProfile(false); } }; const handleActivateProfile = async (profileId) => { setActivatingProfile(profileId); setMessage(null); try { const res = await fetch("/api/cli-tools/codex-profiles", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ profileId }), }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: data.message || t("profileActivated") }); checkCodexStatus(); fetchBackups(); } else { setMessage({ type: "error", text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failedActivateProfile"), }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setActivatingProfile(null); } }; const handleDeleteProfile = async (profileId) => { try { const res = await fetch("/api/cli-tools/codex-profiles", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ profileId }), }); if (res.ok) fetchProfiles(); } catch (error) { console.log("Error deleting profile:", error); } }; // ── Backups ── const fetchBackups = async () => { try { const res = await fetch("/api/cli-tools/backups?tool=codex"); const data = await res.json(); if (res.ok) setBackups(data.backups || []); } catch (error) { console.log("Error fetching backups:", error); } }; const handleRestoreBackup = async (backupId) => { setRestoringBackup(backupId); setMessage(null); try { const res = await fetch("/api/cli-tools/backups", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tool: "codex", backupId }), }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: t("backupRestored") }); checkCodexStatus(); fetchBackups(); } else { setMessage({ type: "error", text: (typeof data.error === "string" ? data.error : data.error?.message) || t("failedRestore"), }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setRestoringBackup(null); } }; const getManualConfigs = () => { const keyToUse = !cloudEnabled ? "sk_omniroute" : ""; let configContent = `# OmniRoute Configuration for Codex CLI model = "${selectedModel || CODEX_DEFAULT_MODELS[0]}"`; if (reasoningEffort && reasoningEffort !== "none") { configContent += `\nmodel_reasoning_effort = "${reasoningEffort}"`; } if (wireApi === "responses") { configContent += ` model_provider = "omniroute" [model_providers.omniroute] name = "OmniRoute" base_url = "${getEffectiveBaseUrl()}" wire_api = "responses" env_key = "OPENAI_API_KEY" `; } else { configContent += ` # Utilize the built-in OpenAI provider pointed to OmniRoute openai_base_url = "${getEffectiveBaseUrl()}" `; } if (Object.keys(modelMappings).length > 0) { configContent += "\n[notice.model_migrations]\n"; for (const [from, to] of Object.entries(modelMappings)) { if (to) { configContent += `"${from}" = "${to}"\n`; } } } const authContent = JSON.stringify({ OPENAI_API_KEY: keyToUse }, null, 2); return [ { filename: "~/.codex/config.toml", content: configContent, }, { filename: "~/.codex/auth.json", content: authContent, }, ]; }; return (

{tool.name}

{t("toolDescriptions.codex")}

expand_more
{isExpanded && (
{checkingCodex && (
progress_activity {t("checkingCli", { tool: "Codex" })}
)} {!checkingCodex && codexStatus && !cliReady && (
warning

{codexStatus.installed ? t("cliNotRunnable", { tool: "Codex" }) : t("cliNotInstalled", { tool: "Codex" })}

{codexStatus.installed ? t("cliFoundFailedHealthcheck", { tool: "Codex", reason: codexStatus.reason ? ` (${codexStatus.reason})` : "", }) : t("installCodexPrompt")}

{showInstallGuide && (

{t("installationGuide")}

{t("platforms")}

npm install -g @openai/codex

{t("afterInstallationRun")}{" "} codex{" "} {t("toVerify")}

{t("codexAuthNotePrefix")}{" "} ~/.codex/auth.json {" "} {t("codexAuthNoteMiddle")}{" "} OPENAI_API_KEY . {t("codexAuthNoteSuffix")}

)}
)} {!checkingCodex && cliReady && ( <>
{/* Current Base URL */} {codexStatus?.config && (() => { const parsed = codexStatus.config.match(/base_url\s*=\s*"([^"]+)"/); const currentBaseUrl = parsed ? parsed[1] : null; return currentBaseUrl ? (
{t("current")} arrow_forward {currentBaseUrl}
) : null; })()} {/* Base URL */}
{t("baseUrl")} arrow_forward setCustomBaseUrl(e.target.value)} placeholder={t("baseUrlPlaceholder")} className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> {customBaseUrl && getDisplayUrl() !== normalizeCodexBaseUrl(baseUrl, wireApi) && ( )}
{/* API Key */}
{t("apiKey")} arrow_forward {apiKeys.length > 0 ? ( ) : ( {cloudEnabled ? t("noApiKeysCreateOne") : t("defaultOmnirouteKey")} )}
{/* Default Model */}
{t("model")} arrow_forward setSelectedModel(e.target.value)} placeholder="gpt-5.5" className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> {selectedModel && ( )}
{/* Reasoning Effort */}
Reasoning Effort arrow_forward
{/* Wire API */}
Wire API arrow_forward
Model Aliases ([notice.model_migrations])
{CODEX_DEFAULT_MODELS.map((defaultModel) => (
{defaultModel} arrow_forward setModelMappings({ ...modelMappings, [defaultModel]: e.target.value }) } placeholder={`Route ${defaultModel} to...`} className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" /> {modelMappings[defaultModel] && ( )}
))}
{message && (
{message.type === "success" ? "check_circle" : "error"} {message.text}
)}
{/* Profiles Section */} {showProfiles && (

manage_accounts {t("savedProfiles")}

{profiles.length === 0 ? (

{t("noProfilesYet")}

) : (
{profiles.map((p) => (
person {p.name} {p.authLabel}
))}
)}
setNewProfileName(e.target.value)} placeholder={t("profileNamePlaceholder")} className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50" onKeyDown={(e) => e.key === "Enter" && handleSaveProfile()} />
)} {/* Backups Section */} {showBackups && (

history {t("configBackups")}

{backups.length === 0 ? (

{t("noBackupsYet")}

) : (
{backups.map((b) => (
description {b.id} {new Date(b.createdAt).toLocaleString()}
))}
)}
)} )}
)} setModalOpen(false)} onSelect={handleModelSelect} selectedModel={selectedModel} activeProviders={activeProviders} modelAliases={modelAliases} title={t("selectModelForTool", { tool: "Codex" })} /> setShowManualConfigModal(false)} title={t("codexManualConfiguration")} configs={getManualConfigs()} /> ); }