Files
ccs/ui/src/components/compatible-cli/codex-top-level-controls-card.tsx
T
Tam Nhu Tran c3401f0a91 feat(ui): complete dashboard i18n coverage across 162 files
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
2026-04-13 20:52:30 -04:00

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>
);
}