mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 00:16:46 +00:00
Replace all remaining hardcoded English strings with t() calls across the CCS dashboard UI. Add zh-CN, vi, ja translations for all new keys. - 162 files updated (components, hooks, pages, lib utilities) - 7 domains covered: layout/shared, accounts/auth, CLIProxy, compatible CLI (Codex/Cursor/Droid/Copilot), logs/monitoring, analytics, and lib utilities - 4 locales: en, zh-CN, vi, ja with natural (not literal) translations - Fix duplicate customPresetDialog key shadowing in en/zh-CN - Fix missing ja nav.logs and providerModelSelector keys - Fix openrouter-banner count type (string → number) - Fix add-account-dialog double setLocalError call - Fix codex-model-providers-card missing useTranslation Closes #983
572 lines
22 KiB
TypeScript
572 lines
22 KiB
TypeScript
import { useState } from 'react';
|
|
import { CircleAlert, Loader2, SlidersHorizontal } from 'lucide-react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select';
|
|
import type { CodexTopLevelSettingsPatch } from '@/hooks/use-codex-types';
|
|
import type { CodexTopLevelSettingsView } from '@/lib/codex-config';
|
|
import { CodexConfigCardShell } from './codex-config-card-shell';
|
|
|
|
const UNSET = '__unset__';
|
|
const GPT_54_MAX_CONTEXT_WINDOW = 1_050_000;
|
|
const GPT_54_STANDARD_CONTEXT_WINDOW = 272_000;
|
|
const CCS_GPT_54_STARTER_CONTEXT_WINDOW = 800_000;
|
|
const CCS_GPT_54_STARTER_AUTO_COMPACT_TOKEN_LIMIT = 700_000;
|
|
const INTEGER_FORMATTER = new Intl.NumberFormat('en-US');
|
|
|
|
interface CodexTopLevelControlsCardProps {
|
|
values: CodexTopLevelSettingsView;
|
|
providerNames: string[];
|
|
disabled?: boolean;
|
|
disabledReason?: string | null;
|
|
saving?: boolean;
|
|
onSave: (values: CodexTopLevelSettingsPatch) => Promise<void> | void;
|
|
}
|
|
|
|
function toSelectValue(value: string | null | undefined) {
|
|
return value ?? UNSET;
|
|
}
|
|
|
|
function withCurrentValue(options: string[], current: string | null | undefined) {
|
|
return current && !options.includes(current) ? [current, ...options] : options;
|
|
}
|
|
|
|
function formatInteger(value: number) {
|
|
return INTEGER_FORMATTER.format(value);
|
|
}
|
|
|
|
function isGpt54ModelId(value: string | null | undefined) {
|
|
return value?.trim().toLowerCase().startsWith('gpt-5.4') ?? false;
|
|
}
|
|
|
|
function buildTopLevelPatch(
|
|
initialValues: CodexTopLevelSettingsView,
|
|
draft: CodexTopLevelSettingsView
|
|
): CodexTopLevelSettingsPatch {
|
|
const patch: CodexTopLevelSettingsPatch = {};
|
|
|
|
if (draft.model !== initialValues.model) patch.model = draft.model;
|
|
if (draft.modelReasoningEffort !== initialValues.modelReasoningEffort) {
|
|
patch.modelReasoningEffort = draft.modelReasoningEffort;
|
|
}
|
|
if (draft.modelContextWindow !== initialValues.modelContextWindow) {
|
|
patch.modelContextWindow = draft.modelContextWindow;
|
|
}
|
|
if (draft.modelAutoCompactTokenLimit !== initialValues.modelAutoCompactTokenLimit) {
|
|
patch.modelAutoCompactTokenLimit = draft.modelAutoCompactTokenLimit;
|
|
}
|
|
if (draft.modelProvider !== initialValues.modelProvider) {
|
|
patch.modelProvider = draft.modelProvider;
|
|
}
|
|
if (draft.approvalPolicy !== initialValues.approvalPolicy) {
|
|
patch.approvalPolicy = draft.approvalPolicy;
|
|
}
|
|
if (draft.sandboxMode !== initialValues.sandboxMode) patch.sandboxMode = draft.sandboxMode;
|
|
if (draft.webSearch !== initialValues.webSearch) patch.webSearch = draft.webSearch;
|
|
if (draft.toolOutputTokenLimit !== initialValues.toolOutputTokenLimit) {
|
|
patch.toolOutputTokenLimit = draft.toolOutputTokenLimit;
|
|
}
|
|
if (draft.personality !== initialValues.personality) patch.personality = draft.personality;
|
|
|
|
return patch;
|
|
}
|
|
|
|
interface TopLevelControlsFormProps {
|
|
initialValues: CodexTopLevelSettingsView;
|
|
providerNames: string[];
|
|
disabled: boolean;
|
|
saving: boolean;
|
|
onSave: (values: CodexTopLevelSettingsPatch) => Promise<void> | void;
|
|
}
|
|
|
|
function TopLevelControlsForm({
|
|
initialValues,
|
|
providerNames,
|
|
disabled,
|
|
saving,
|
|
onSave,
|
|
}: TopLevelControlsFormProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const [draft, setDraft] = useState<CodexTopLevelSettingsView>(initialValues);
|
|
const reasoningOptions = withCurrentValue(
|
|
['minimal', 'low', 'medium', 'high', 'xhigh'],
|
|
draft.modelReasoningEffort
|
|
);
|
|
const providerOptions = withCurrentValue(providerNames, draft.modelProvider);
|
|
const approvalOptions = withCurrentValue(
|
|
['on-request', 'never', 'untrusted'],
|
|
draft.approvalPolicy
|
|
);
|
|
const sandboxOptions = withCurrentValue(
|
|
['read-only', 'workspace-write', 'danger-full-access'],
|
|
draft.sandboxMode
|
|
);
|
|
const webSearchOptions = withCurrentValue(['cached', 'live', 'disabled'], draft.webSearch);
|
|
const personalityOptions = withCurrentValue(['none', 'friendly', 'pragmatic'], draft.personality);
|
|
const patch = buildTopLevelPatch(initialValues, draft);
|
|
const hasChanges = Object.keys(patch).length > 0;
|
|
const isGpt54Selected = isGpt54ModelId(draft.model);
|
|
const parseOptionalInteger = (value: string) => {
|
|
const trimmed = value.trim();
|
|
return trimmed.length > 0 ? Number(trimmed) : null;
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
<div className="space-y-1">
|
|
<p className="text-xs font-medium">{t('codex.model')}</p>
|
|
<Input
|
|
value={draft.model ?? ''}
|
|
onChange={(event) =>
|
|
setDraft((current) => ({ ...current, model: event.target.value || null }))
|
|
}
|
|
placeholder="gpt-5.4"
|
|
disabled={disabled}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<p className="text-xs font-medium">{t('codex.reasoningEffort')}</p>
|
|
<Select
|
|
value={toSelectValue(draft.modelReasoningEffort)}
|
|
onValueChange={(next) =>
|
|
setDraft((current) => ({
|
|
...current,
|
|
modelReasoningEffort: next === UNSET ? null : next,
|
|
}))
|
|
}
|
|
disabled={disabled}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={t('codex.useDefault')} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value={UNSET}>{t('codex.useDefault')}</SelectItem>
|
|
{reasoningOptions.map((value) => (
|
|
<SelectItem key={value} value={value}>
|
|
{value}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<p className="text-xs font-medium">{t('codex.defaultProvider')}</p>
|
|
<Select
|
|
value={toSelectValue(draft.modelProvider)}
|
|
onValueChange={(next) =>
|
|
setDraft((current) => ({ ...current, modelProvider: next === UNSET ? null : next }))
|
|
}
|
|
disabled={disabled}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={t('codex.useCodexDefault')} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value={UNSET}>{t('codex.useCodexDefault')}</SelectItem>
|
|
{providerOptions.map((name) => (
|
|
<SelectItem key={name} value={name}>
|
|
{name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<p className="text-xs font-medium">{t('codex.approvalPolicy')}</p>
|
|
<Select
|
|
value={toSelectValue(draft.approvalPolicy)}
|
|
onValueChange={(next) =>
|
|
setDraft((current) => ({ ...current, approvalPolicy: next === UNSET ? null : next }))
|
|
}
|
|
disabled={disabled}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={t('codex.useDefault')} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value={UNSET}>{t('codex.useDefault')}</SelectItem>
|
|
{approvalOptions.map((value) => (
|
|
<SelectItem key={value} value={value}>
|
|
{value}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<p className="text-xs font-medium">{t('codex.sandboxMode')}</p>
|
|
<Select
|
|
value={toSelectValue(draft.sandboxMode)}
|
|
onValueChange={(next) =>
|
|
setDraft((current) => ({ ...current, sandboxMode: next === UNSET ? null : next }))
|
|
}
|
|
disabled={disabled}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={t('codex.useDefault')} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value={UNSET}>{t('codex.useDefault')}</SelectItem>
|
|
{sandboxOptions.map((value) => (
|
|
<SelectItem key={value} value={value}>
|
|
{value}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<p className="text-xs font-medium">{t('codex.webSearch')}</p>
|
|
<Select
|
|
value={toSelectValue(draft.webSearch)}
|
|
onValueChange={(next) =>
|
|
setDraft((current) => ({ ...current, webSearch: next === UNSET ? null : next }))
|
|
}
|
|
disabled={disabled}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={t('codex.useDefault')} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value={UNSET}>{t('codex.useDefault')}</SelectItem>
|
|
{webSearchOptions.map((value) => (
|
|
<SelectItem key={value} value={value}>
|
|
{value}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<p className="text-xs font-medium">{t('codex.toolOutputTokenLimit')}</p>
|
|
<Input
|
|
type="number"
|
|
min={1}
|
|
value={draft.toolOutputTokenLimit ?? ''}
|
|
onChange={(event) =>
|
|
setDraft((current) => ({
|
|
...current,
|
|
toolOutputTokenLimit: event.target.value ? Number(event.target.value) : null,
|
|
}))
|
|
}
|
|
placeholder="25000"
|
|
disabled={disabled}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<p className="text-xs font-medium">{t('codex.personality')}</p>
|
|
<Select
|
|
value={toSelectValue(draft.personality)}
|
|
onValueChange={(next) =>
|
|
setDraft((current) => ({ ...current, personality: next === UNSET ? null : next }))
|
|
}
|
|
disabled={disabled}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder={t('codex.useDefault')} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value={UNSET}>{t('codex.useDefault')}</SelectItem>
|
|
{personalityOptions.map((value) => (
|
|
<SelectItem key={value} value={value}>
|
|
{value}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4 rounded-xl border border-amber-500/30 bg-amber-500/5 p-4 shadow-sm dark:bg-amber-400/5">
|
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
<div className="space-y-2">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<CircleAlert className="h-4 w-4 text-amber-600 dark:text-amber-300" />
|
|
<p className="text-sm font-semibold">{t('codex.longContextOverride')}</p>
|
|
<Badge
|
|
variant="outline"
|
|
className="border-amber-500/40 bg-background/80 text-[10px] uppercase tracking-[0.16em] text-amber-700 dark:text-amber-300"
|
|
>
|
|
{/* TODO i18n: missing key codex.manualOptInOnly */}
|
|
Manual opt-in only
|
|
</Badge>
|
|
<Badge
|
|
variant="secondary"
|
|
className="text-[10px] uppercase tracking-[0.16em] text-muted-foreground"
|
|
>
|
|
{/* TODO i18n: missing keys codex.gpt54Selected / codex.gpt54Reference */}
|
|
{isGpt54Selected ? 'GPT-5.4 selected' : 'GPT-5.4 reference'}
|
|
</Badge>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{/* TODO i18n: missing key codex.draftValuesOnly */}
|
|
Draft values only. Nothing applies until Save.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={disabled}
|
|
onClick={() =>
|
|
setDraft((current) => ({
|
|
...current,
|
|
modelContextWindow: CCS_GPT_54_STARTER_CONTEXT_WINDOW,
|
|
modelAutoCompactTokenLimit: CCS_GPT_54_STARTER_AUTO_COMPACT_TOKEN_LIMIT,
|
|
}))
|
|
}
|
|
>
|
|
{/* TODO i18n: missing key codex.fillCautiousPair */}
|
|
Fill cautious pair
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="sm"
|
|
disabled={disabled}
|
|
onClick={() =>
|
|
setDraft((current) => ({
|
|
...current,
|
|
modelContextWindow: GPT_54_MAX_CONTEXT_WINDOW,
|
|
}))
|
|
}
|
|
>
|
|
{/* TODO i18n: missing key codex.setOfficialMaxWindow */}
|
|
Set official max window
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
disabled={disabled}
|
|
onClick={() =>
|
|
setDraft((current) => ({
|
|
...current,
|
|
modelContextWindow: null,
|
|
modelAutoCompactTokenLimit: null,
|
|
}))
|
|
}
|
|
>
|
|
{/* TODO i18n: missing key codex.clear */}
|
|
Clear
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-2 sm:grid-cols-3">
|
|
<div className="rounded-lg border bg-background/85 px-3 py-3 shadow-sm shadow-black/5">
|
|
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
{/* TODO i18n: missing key codex.officialMax */}
|
|
Official max
|
|
</p>
|
|
<p className="mt-1 font-mono text-base font-semibold text-foreground">1.05M / 1M</p>
|
|
<p className="mt-1 text-[11px] text-muted-foreground">{t('codex.gptContextCap')}</p>
|
|
</div>
|
|
<div className="rounded-lg border bg-background/85 px-3 py-3 shadow-sm shadow-black/5">
|
|
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
{/* TODO i18n: missing key codex.standardWindow */}
|
|
Standard window
|
|
</p>
|
|
<p className="mt-1 font-mono text-base font-semibold text-foreground">
|
|
{formatInteger(GPT_54_STANDARD_CONTEXT_WINDOW)}
|
|
</p>
|
|
<p className="mt-1 text-[11px] text-muted-foreground">{t('codex.normalUsageWindow')}</p>
|
|
</div>
|
|
<div className="rounded-lg border bg-background/85 px-3 py-3 shadow-sm shadow-black/5">
|
|
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
{/* TODO i18n: missing key codex.above272k */}
|
|
Above 272K
|
|
</p>
|
|
<p className="mt-1 font-mono text-base font-semibold text-foreground">
|
|
{t('codex.counts2x')}
|
|
</p>
|
|
<p className="mt-1 text-[11px] text-muted-foreground">{t('codex.usageLimitCost')}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-3 rounded-lg border bg-background/75 px-3 py-3">
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
{/* TODO i18n: missing key codex.oneCautiousPair */}
|
|
One cautious pair
|
|
</p>
|
|
<div className="rounded-full border bg-background px-2.5 py-1 font-mono text-[11px] font-medium">
|
|
{/* TODO i18n: missing key codex.context */}
|
|
Context {formatInteger(CCS_GPT_54_STARTER_CONTEXT_WINDOW)}
|
|
</div>
|
|
<div className="rounded-full border bg-background px-2.5 py-1 font-mono text-[11px] font-medium">
|
|
{/* TODO i18n: missing key codex.autoCompact */}
|
|
Auto-compact {formatInteger(CCS_GPT_54_STARTER_AUTO_COMPACT_TOKEN_LIMIT)}
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
<Badge
|
|
variant="outline"
|
|
className="border-border/70 bg-background/80 text-[10px] uppercase tracking-[0.14em] text-muted-foreground"
|
|
>
|
|
{/* TODO i18n: missing key codex.notOfficial */}
|
|
Not official
|
|
</Badge>
|
|
<Badge
|
|
variant="outline"
|
|
className="border-border/70 bg-background/80 text-[10px] uppercase tracking-[0.14em] text-muted-foreground"
|
|
>
|
|
{/* TODO i18n: missing key codex.draftOnly */}
|
|
Draft only
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
|
|
<span>{t('codex.quickFillWarning')}</span>
|
|
{!isGpt54Selected && draft.model ? (
|
|
<span>
|
|
{/* TODO i18n: missing key codex.shouldBeCheckedSeparately */}
|
|
<code>{draft.model}</code> should be checked separately.
|
|
</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
<div className="space-y-1">
|
|
<p className="text-xs font-medium">{t('codex.modelContextWindow')}</p>
|
|
<Input
|
|
aria-label={t('codex.modelContextWindow')}
|
|
type="number"
|
|
min={1}
|
|
value={draft.modelContextWindow ?? ''}
|
|
onChange={(event) =>
|
|
setDraft((current) => ({
|
|
...current,
|
|
modelContextWindow: parseOptionalInteger(event.target.value),
|
|
}))
|
|
}
|
|
// TODO i18n: missing key codex.unset
|
|
placeholder="Unset"
|
|
disabled={disabled}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
{/* TODO i18n: missing key codex.writesModelContextWindow */}
|
|
Writes <code>model_context_window</code>. Leave unset to keep Codex defaults.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<p className="text-xs font-medium">{t('codex.autoCompactTokenLimit')}</p>
|
|
<Input
|
|
aria-label={t('codex.autoCompactTokenLimit')}
|
|
type="number"
|
|
min={1}
|
|
value={draft.modelAutoCompactTokenLimit ?? ''}
|
|
onChange={(event) =>
|
|
setDraft((current) => ({
|
|
...current,
|
|
modelAutoCompactTokenLimit: parseOptionalInteger(event.target.value),
|
|
}))
|
|
}
|
|
// TODO i18n: missing key codex.unset
|
|
placeholder="Unset"
|
|
disabled={disabled}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
{/* TODO i18n: missing key codex.writesAutoCompactTokenLimit */}
|
|
Writes <code>model_auto_compact_token_limit</code>. Leave unset to keep model
|
|
defaults.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
|
|
{/* TODO i18n: missing key codex.docs */}
|
|
<span className="text-[10px] uppercase tracking-[0.14em]">Docs</span>
|
|
<a
|
|
href="https://developers.openai.com/api/docs/models/gpt-5.4"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="underline underline-offset-2 hover:text-foreground"
|
|
>
|
|
{/* TODO i18n: missing key codex.gpt54ModelPage */}
|
|
GPT-5.4 model page
|
|
</a>
|
|
<a
|
|
href="https://openai.com/index/introducing-gpt-5-4/"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="underline underline-offset-2 hover:text-foreground"
|
|
>
|
|
{/* TODO i18n: missing key codex.releaseNotes */}
|
|
Release notes
|
|
</a>
|
|
<a
|
|
href="https://developers.openai.com/codex/config-reference"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
className="underline underline-offset-2 hover:text-foreground"
|
|
>
|
|
{/* TODO i18n: missing key codex.configReference */}
|
|
Config reference
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end">
|
|
<Button onClick={() => onSave(patch)} disabled={disabled || saving || !hasChanges}>
|
|
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
|
{/* TODO i18n: missing key codex.saveTopLevelSettings */}
|
|
Save top-level settings
|
|
</Button>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function CodexTopLevelControlsCard({
|
|
values,
|
|
providerNames,
|
|
disabled = false,
|
|
disabledReason,
|
|
saving = false,
|
|
onSave,
|
|
}: CodexTopLevelControlsCardProps) {
|
|
return (
|
|
<CodexConfigCardShell
|
|
// TODO i18n: missing key codex.topLevelControls
|
|
title="Top-level controls"
|
|
badge="config.toml"
|
|
icon={<SlidersHorizontal className="h-4 w-4" />}
|
|
// TODO i18n: missing key codex.topLevelControlsDesc
|
|
description="Structured controls for the stable top-level Codex settings users touch most often. Unsupported upstream shapes stay untouched and should be edited in raw TOML."
|
|
disabledReason={disabledReason}
|
|
>
|
|
<TopLevelControlsForm
|
|
key={JSON.stringify(values)}
|
|
initialValues={values}
|
|
providerNames={providerNames}
|
|
disabled={disabled}
|
|
saving={saving}
|
|
onSave={onSave}
|
|
/>
|
|
</CodexConfigCardShell>
|
|
);
|
|
}
|