mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 04:18:05 +00:00
fix(ui): finish dashboard i18n follow-up pass
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# Dashboard i18n Guide
|
||||
|
||||
Last Updated: 2026-03-02
|
||||
Last Updated: 2026-04-14
|
||||
|
||||
This document describes the internationalization (i18n) architecture used by the CCS Dashboard (`ui/`), how locale selection works, and how to add new languages safely.
|
||||
|
||||
@@ -14,6 +14,7 @@ Dashboard i18n currently covers UI text rendered by React components.
|
||||
- `en` (English)
|
||||
- `zh-CN` (Simplified Chinese)
|
||||
- `vi` (Vietnamese)
|
||||
- `ja` (Japanese)
|
||||
- Locale state is persisted in browser localStorage using `ccs-ui-locale`.
|
||||
- Fallback language is `en`.
|
||||
|
||||
@@ -38,6 +39,7 @@ Out of scope:
|
||||
- `en.translation`
|
||||
- `zh-CN.translation`
|
||||
- `vi.translation`
|
||||
- `ja.translation`
|
||||
- Uses `initReactI18next` for React integration.
|
||||
|
||||
### Locale utilities
|
||||
@@ -54,6 +56,12 @@ Out of scope:
|
||||
- Uses `react-i18next` + shadcn `Select`.
|
||||
- Calls `persistLocale` and `i18n.changeLanguage` on selection.
|
||||
|
||||
### Test bootstrap
|
||||
|
||||
- File: `ui/tests/setup/vitest-setup.ts`
|
||||
- Test setup must import `ui/src/lib/i18n.ts` so direct `useTranslation()` consumers resolve the same singleton instance as the app.
|
||||
- If a test mocks `useTranslation()`, keep the mocked key surface aligned with the component output or the assertions will drift.
|
||||
|
||||
---
|
||||
|
||||
## Key Conventions
|
||||
|
||||
@@ -30,12 +30,10 @@ export function CodexFeaturesCard({
|
||||
|
||||
return (
|
||||
<CodexConfigCardShell
|
||||
// TODO i18n: missing key codex.features
|
||||
title="Features"
|
||||
title={t('codex.features')}
|
||||
badge="features"
|
||||
icon={<Sparkles className="h-4 w-4" />}
|
||||
// TODO i18n: missing key codex.featuresDesc
|
||||
description="Toggle the supported Codex feature flags CCS can safely manage."
|
||||
description={t('codex.featuresDesc')}
|
||||
disabledReason={disabledReason}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
@@ -81,14 +79,9 @@ export function CodexFeaturesCard({
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{/* TODO i18n: missing key codex.configOnlyFlags */}
|
||||
Existing config-only flags
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{/* TODO i18n: missing key codex.configOnlyFlagsDesc */}
|
||||
These feature keys already exist in your `config.toml`, so CCS can surface them
|
||||
without claiming full catalog coverage.
|
||||
{t('codex.configOnlyFlags')}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t('codex.configOnlyFlagsDesc')}</p>
|
||||
</div>
|
||||
{configOnlyFeatures.map(([name, current]) => (
|
||||
<div
|
||||
@@ -99,15 +92,11 @@ export function CodexFeaturesCard({
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-medium">{name}</p>
|
||||
<Badge variant="secondary" className="text-[10px]">
|
||||
{/* TODO i18n: missing key codex.existing */}
|
||||
existing
|
||||
{t('codex.existing')}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{/* TODO i18n: missing keys codex.nonBooleanForm / codex.discoveredFromFile */}
|
||||
{current === null
|
||||
? 'Stored in a non-boolean form. Use raw TOML if you need to edit it.'
|
||||
: "Discovered from the current file instead of CCS's built-in catalog."}
|
||||
{current === null ? t('codex.nonBooleanForm') : t('codex.discoveredFromFile')}
|
||||
</p>
|
||||
</div>
|
||||
{current === null ? (
|
||||
|
||||
@@ -74,27 +74,21 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Info className="h-4 w-4" />
|
||||
{/* TODO i18n: missing key codex.howCodexWorks */}
|
||||
How Codex works in CCS
|
||||
{t('codex.howCodexWorks')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
<ul className="ml-4 list-disc space-y-1.5 [&>li]:pl-1">
|
||||
<li>{t('codex.nativeDesc')}</li>
|
||||
<li>
|
||||
{/* TODO i18n: missing key codex.nativeConfigLabel */}
|
||||
<strong>Native config:</strong> <code>ccs-codex</code> and <code>ccsx</code> launch
|
||||
native Codex using your saved defaults.
|
||||
<strong>{t('codex.nativeConfigLabel')}</strong> {t('codex.nativeConfigDesc')}
|
||||
</li>
|
||||
<li>
|
||||
{/* TODO i18n: missing key codex.transientOverrides */}
|
||||
<strong>Transient overrides:</strong> <code>ccsxp</code> (or{' '}
|
||||
<code>ccs codex --target codex</code>) uses the CCS provider shortcut.
|
||||
<strong>{t('codex.transientOverridesLabel')}</strong>{' '}
|
||||
{t('codex.transientOverridesDesc')}
|
||||
</li>
|
||||
<li>
|
||||
{/* TODO i18n: missing key codex.cliproxyDefault */}
|
||||
<strong>CLIProxy default:</strong> To make plain <code>codex</code> use CLIProxy,
|
||||
set <code>model_provider = "cliproxy"</code> and add the recipe below.
|
||||
<strong>{t('codex.cliproxyDefaultLabel')}</strong> {t('codex.cliproxyDefaultDesc')}
|
||||
</li>
|
||||
<li>{t('codex.apiProfilesDefault')}</li>
|
||||
</ul>
|
||||
@@ -105,34 +99,42 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<TerminalSquare className="h-4 w-4" />
|
||||
{/* TODO i18n: missing key codex.runtimeInstall */}
|
||||
Runtime install
|
||||
{t('codex.runtimeInstall')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">{t('codex.status')}</span>
|
||||
<Badge variant={diagnostics.binary.installed ? 'default' : 'secondary'}>
|
||||
{/* TODO i18n: missing keys codex.detected / codex.notFound */}
|
||||
{diagnostics.binary.installed ? 'Detected' : 'Not found'}
|
||||
{diagnostics.binary.installed ? t('codex.detected') : t('codex.notFound')}
|
||||
</Badge>
|
||||
</div>
|
||||
{/* TODO i18n: missing keys codex.detectionSource / codex.binaryPath / codex.installDirectory / codex.version / codex.nativeAliases / codex.ccsProviderShortcut / codex.configOverrideSupport */}
|
||||
<DetailRow label="Detection source" value={diagnostics.binary.source} mono />
|
||||
<DetailRow label="Binary path" value={diagnostics.binary.path || 'Not found'} mono />
|
||||
<DetailRow label={t('codex.detectionSource')} value={diagnostics.binary.source} mono />
|
||||
<DetailRow
|
||||
label="Install directory"
|
||||
label={t('codex.binaryPath')}
|
||||
value={diagnostics.binary.path || t('codex.notFound')}
|
||||
mono
|
||||
/>
|
||||
<DetailRow
|
||||
label={t('codex.installDirectory')}
|
||||
value={diagnostics.binary.installDir || 'N/A'}
|
||||
mono
|
||||
/>
|
||||
<DetailRow label="Version" value={diagnostics.binary.version || 'Unknown'} mono />
|
||||
<DetailRow label="Native aliases" value="ccs-codex, ccsx" mono />
|
||||
<DetailRow label="CCS provider shortcut" value="ccsxp" mono />
|
||||
<DetailRow
|
||||
label={t('codex.versionLabel')}
|
||||
value={diagnostics.binary.version || 'Unknown'}
|
||||
mono
|
||||
/>
|
||||
<DetailRow label={t('codex.nativeAliases')} value="ccs-codex, ccsx" mono />
|
||||
<DetailRow label={t('codex.ccsProviderShortcut')} value="ccsxp" mono />
|
||||
<div className="flex items-center justify-between rounded-md border px-3 py-2">
|
||||
<span className="text-sm text-muted-foreground">--config override support</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t('codex.configOverrideSupport')}
|
||||
</span>
|
||||
<Badge variant={diagnostics.binary.supportsConfigOverrides ? 'default' : 'secondary'}>
|
||||
{/* TODO i18n: missing keys codex.available / codex.missing */}
|
||||
{diagnostics.binary.supportsConfigOverrides ? 'Available' : 'Missing'}
|
||||
{diagnostics.binary.supportsConfigOverrides
|
||||
? t('codex.available')
|
||||
: t('codex.missing')}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -142,8 +144,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Route className="h-4 w-4" />
|
||||
{/* TODO i18n: missing key codex.cliproxyNativeCodex */}
|
||||
CLIProxy-backed native Codex
|
||||
{t('codex.cliproxyNativeCodex')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||
@@ -155,14 +156,10 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
</p>
|
||||
<ul className="ml-4 list-disc space-y-1 [&>li]:pl-1">
|
||||
<li>
|
||||
{/* TODO i18n: missing key codex.builtInCcsxp */}
|
||||
<strong>Built-in:</strong> Use <code>ccsxp</code> for the CCS provider
|
||||
shortcut.
|
||||
<strong>{t('codex.builtInLabel')}</strong> {t('codex.builtInCcsxpDesc')}
|
||||
</li>
|
||||
<li>
|
||||
{/* TODO i18n: missing key codex.nativeRecipe */}
|
||||
<strong>Native:</strong> Configure the recipe below to use CLIProxy directly
|
||||
with <code>codex</code>.
|
||||
<strong>{t('codex.nativeRecipeLabel')}</strong> {t('codex.nativeRecipeDesc')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -173,29 +170,13 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
</pre>
|
||||
</div>
|
||||
<ol className="ml-4 list-decimal space-y-1.5 [&>li]:pl-1">
|
||||
<li>
|
||||
{/* TODO i18n: missing key codex.saveProviderNamedCliproxy */}
|
||||
Save a provider named <code>cliproxy</code> with the base URL and env key above.
|
||||
</li>
|
||||
<li>
|
||||
{/* TODO i18n: missing key codex.inTopLevelSetDefault */}
|
||||
In <strong>Top-level settings</strong>, set <strong>Default provider</strong> to{' '}
|
||||
<code>cliproxy</code>.
|
||||
</li>
|
||||
<li>
|
||||
{/* TODO i18n: missing key codex.exportCliproxyApiKey */}
|
||||
Export <code>CLIPROXY_API_KEY</code> in your shell before launching native
|
||||
Codex.
|
||||
</li>
|
||||
<li>{t('codex.saveProviderNamedCliproxy')}</li>
|
||||
<li>{t('codex.inTopLevelSetDefault')}</li>
|
||||
<li>{t('codex.exportCliproxyApiKey')}</li>
|
||||
</ol>
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
{/* TODO i18n: missing key codex.noConfigOverrides */}
|
||||
This Codex build can still use the native path, but CCS-backed Codex routing via{' '}
|
||||
<code>ccsxp</code> or <code>ccs codex --target codex</code> stays unavailable until
|
||||
the detected Codex binary exposes <code>--config</code> overrides.
|
||||
</p>
|
||||
<p>{t('codex.noConfigOverrides')}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -204,8 +185,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Folder className="h-4 w-4" />
|
||||
{/* TODO i18n: missing key codex.configFile */}
|
||||
Config file
|
||||
{t('codex.configFile')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
@@ -218,21 +198,21 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
<XCircle className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
{/* TODO i18n: missing keys codex.path / codex.resolved / codex.size / codex.lastModified */}
|
||||
<DetailRow label="Path" value={diagnostics.file.path} mono />
|
||||
<DetailRow label="Resolved" value={diagnostics.file.resolvedPath} mono />
|
||||
<DetailRow label="Size" value={formatBytes(diagnostics.file.sizeBytes)} />
|
||||
<DetailRow label="Last modified" value={formatTimestamp(diagnostics.file.mtimeMs)} />
|
||||
<DetailRow label={t('codex.path')} value={diagnostics.file.path} mono />
|
||||
<DetailRow label={t('codex.resolved')} value={diagnostics.file.resolvedPath} mono />
|
||||
<DetailRow label={t('codex.size')} value={formatBytes(diagnostics.file.sizeBytes)} />
|
||||
<DetailRow
|
||||
label={t('codex.lastModified')}
|
||||
value={formatTimestamp(diagnostics.file.mtimeMs)}
|
||||
/>
|
||||
{diagnostics.file.parseError && (
|
||||
<p className="text-xs text-amber-600">
|
||||
{/* TODO i18n: missing key codex.tomlWarning */}
|
||||
TOML warning: {diagnostics.file.parseError}
|
||||
{t('codex.tomlWarning')}: {diagnostics.file.parseError}
|
||||
</p>
|
||||
)}
|
||||
{diagnostics.file.readError && (
|
||||
<p className="text-xs text-destructive">
|
||||
{/* TODO i18n: missing key codex.readWarning */}
|
||||
Read warning: {diagnostics.file.readError}
|
||||
{t('codex.readWarning')}: {diagnostics.file.readError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -243,64 +223,61 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<ShieldCheck className="h-4 w-4" />
|
||||
{/* TODO i18n: missing key codex.currentUserLayerSummary */}
|
||||
Current user-layer summary
|
||||
{t('codex.currentUserLayerSummary')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{/* TODO i18n: missing key codex.notSet */}
|
||||
<DetailRow
|
||||
label={t('codex.model')}
|
||||
value={diagnostics.config.model || 'Not set'}
|
||||
value={diagnostics.config.model || t('codex.notSet')}
|
||||
mono
|
||||
/>
|
||||
<DetailRow
|
||||
label={t('codex.defaultProvider')}
|
||||
value={diagnostics.config.modelProvider || 'Not set'}
|
||||
value={diagnostics.config.modelProvider || t('codex.notSet')}
|
||||
mono
|
||||
/>
|
||||
<DetailRow
|
||||
// TODO i18n: missing key codex.activeProfile
|
||||
label="Active profile"
|
||||
value={diagnostics.config.activeProfile || 'Not set'}
|
||||
label={t('codex.activeProfile')}
|
||||
value={diagnostics.config.activeProfile || t('codex.notSet')}
|
||||
mono
|
||||
/>
|
||||
<DetailRow
|
||||
label={t('codex.approvalPolicy')}
|
||||
value={diagnostics.config.approvalPolicy || 'Not set'}
|
||||
value={diagnostics.config.approvalPolicy || t('codex.notSet')}
|
||||
mono
|
||||
/>
|
||||
<DetailRow
|
||||
label={t('codex.sandboxMode')}
|
||||
value={diagnostics.config.sandboxMode || 'Not set'}
|
||||
value={diagnostics.config.sandboxMode || t('codex.notSet')}
|
||||
mono
|
||||
/>
|
||||
<DetailRow
|
||||
label={t('codex.webSearch')}
|
||||
value={diagnostics.config.webSearch || 'Not set'}
|
||||
value={diagnostics.config.webSearch || t('codex.notSet')}
|
||||
mono
|
||||
/>
|
||||
<Separator />
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
{/* TODO i18n: missing keys codex.providersCount / codex.profilesCount / codex.enabledFeaturesCount / codex.mcpServersCount */}
|
||||
<Badge variant="outline" className="justify-center">
|
||||
providers: {diagnostics.config.modelProviderCount}
|
||||
{t('codex.providersCount', { count: diagnostics.config.modelProviderCount })}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="justify-center">
|
||||
profiles: {diagnostics.config.profileCount}
|
||||
{t('codex.profilesCount', { count: diagnostics.config.profileCount })}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="justify-center">
|
||||
enabled features: {diagnostics.config.enabledFeatures.length}
|
||||
{t('codex.enabledFeaturesCount', {
|
||||
count: diagnostics.config.enabledFeatures.length,
|
||||
})}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="justify-center">
|
||||
MCP servers: {diagnostics.config.mcpServerCount}
|
||||
{t('codex.mcpServersCount', { count: diagnostics.config.mcpServerCount })}
|
||||
</Badge>
|
||||
</div>
|
||||
{diagnostics.config.topLevelKeys.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs uppercase tracking-wide text-muted-foreground">
|
||||
{/* TODO i18n: missing key codex.userLayerKeysPresent */}
|
||||
User-layer keys present
|
||||
{t('codex.userLayerKeysPresent')}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{diagnostics.config.topLevelKeys.map((key) => (
|
||||
@@ -317,22 +294,19 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
<QuickCommands
|
||||
snippets={[
|
||||
{
|
||||
// TODO i18n: missing key codex.nativeShortAlias
|
||||
label: 'Native short alias',
|
||||
label: t('codex.nativeShortAlias'),
|
||||
command: 'ccsx',
|
||||
description: 'Launch the short native Codex runtime alias.',
|
||||
},
|
||||
{
|
||||
// TODO i18n: missing key codex.ccsCodexShortcut
|
||||
label: 'CCS Codex shortcut',
|
||||
label: t('codex.ccsCodexShortcut'),
|
||||
command: 'ccsxp "your prompt"',
|
||||
description: supportsManagedRouting
|
||||
? t('codex.runBuiltInCodex')
|
||||
: 'Requires a Codex build that exposes --config overrides.',
|
||||
},
|
||||
{
|
||||
// TODO i18n: missing key codex.explicitProviderRoute
|
||||
label: 'Explicit provider route',
|
||||
label: t('codex.explicitProviderRoute'),
|
||||
command: 'ccs codex --target codex "your prompt"',
|
||||
description: supportsManagedRouting
|
||||
? t('codex.runBuiltInCodexExplicit')
|
||||
@@ -354,8 +328,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<Route className="h-4 w-4" />
|
||||
{/* TODO i18n: missing key codex.runtimeVsProvider */}
|
||||
Runtime vs provider
|
||||
{t('codex.runtimeVsProvider')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 md:grid-cols-2">
|
||||
@@ -373,8 +346,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
</li>
|
||||
</ul>
|
||||
<Badge variant="secondary" className="mt-4 w-fit justify-center font-normal">
|
||||
{/* TODO i18n: missing key codex.honorsSavedNativeConfig */}
|
||||
Honors saved native user config
|
||||
{t('codex.honorsSavedNativeConfig')}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex flex-col rounded-md border p-3 text-sm">
|
||||
@@ -390,15 +362,11 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
</li>
|
||||
</ul>
|
||||
<Badge variant="secondary" className="mt-4 w-fit justify-center font-normal">
|
||||
{/* TODO i18n: missing key codex.usesTransientOverrides */}
|
||||
Uses transient overrides
|
||||
{t('codex.usesTransientOverrides')}
|
||||
</Badge>
|
||||
</>
|
||||
) : (
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
{/* TODO i18n: missing key codex.unavailableNoConfig */}
|
||||
Unavailable (Codex build lacks <code>--config</code> support).
|
||||
</p>
|
||||
<p className="mt-2 text-muted-foreground">{t('codex.unavailableNoConfig')}</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -423,8 +391,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
<TableCell className="font-mono text-xs">{entry.label}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={entry.supported ? 'default' : 'secondary'}>
|
||||
{/* TODO i18n: missing keys common.yes / common.no */}
|
||||
{entry.supported ? 'Yes' : 'No'}
|
||||
{entry.supported ? t('codex.yes') : t('codex.no')}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-xs text-muted-foreground">{entry.notes}</TableCell>
|
||||
@@ -440,8 +407,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-600" />
|
||||
{/* TODO i18n: missing key codex.warnings */}
|
||||
Warnings
|
||||
{t('codex.warningsTitle')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-1.5">
|
||||
|
||||
@@ -120,7 +120,7 @@ function SettingsDialogContent({
|
||||
try {
|
||||
settingsToSave = JSON.parse(rawJsonContent);
|
||||
} catch {
|
||||
throw new Error('Invalid JSON');
|
||||
throw new Error(i18n.t('settingsDialog.invalidJson'));
|
||||
}
|
||||
} else {
|
||||
// Use form-based edits
|
||||
@@ -147,7 +147,7 @@ function SettingsDialogContent({
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to save');
|
||||
throw new Error(i18n.t('settingsDialog.failedSave'));
|
||||
}
|
||||
|
||||
return res.json();
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ArrowUpRight } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import {
|
||||
SUPPORT_SCOPE_LABELS,
|
||||
getSupportScopeLabels,
|
||||
type CliSupportEntry,
|
||||
type SupportScope,
|
||||
} from '@/lib/support-updates-catalog';
|
||||
@@ -23,6 +23,7 @@ const SCOPE_STYLES: Record<SupportScope, string> = {
|
||||
|
||||
export function SupportEntryCard({ entry }: { entry: CliSupportEntry }) {
|
||||
const { t } = useTranslation();
|
||||
const scopeLabels = getSupportScopeLabels();
|
||||
const pillarLabels: { key: keyof CliSupportEntry['pillars']; label: string }[] = [
|
||||
{ key: 'baseUrl', label: t('profileDialog.baseUrl') },
|
||||
{ key: 'auth', label: t('copilotPage.auth') },
|
||||
@@ -41,7 +42,7 @@ export function SupportEntryCard({ entry }: { entry: CliSupportEntry }) {
|
||||
</div>
|
||||
|
||||
<Badge variant="outline" className={SCOPE_STYLES[entry.scope]}>
|
||||
{SUPPORT_SCOPE_LABELS[entry.scope]}
|
||||
{scopeLabels[entry.scope]}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SupportStatusBadge } from '@/components/updates/support-status-badge';
|
||||
import { NoticeProgressBadge } from '@/components/updates/notice-progress-badge';
|
||||
import { UpdatesNoticeActionRow } from '@/components/updates/updates-notice-action-row';
|
||||
import {
|
||||
SUPPORT_SCOPE_LABELS,
|
||||
getSupportScopeLabels,
|
||||
formatCatalogDate,
|
||||
type CliSupportEntry,
|
||||
type SupportNotice,
|
||||
@@ -31,6 +31,7 @@ export function UpdatesDetailsPanel({
|
||||
onUpdateProgress: (nextState: UpdatableNoticeProgress) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const scopeLabels = getSupportScopeLabels();
|
||||
if (!notice) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center rounded-lg border border-dashed bg-muted/20 p-6">
|
||||
@@ -109,7 +110,7 @@ export function UpdatesDetailsPanel({
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="min-w-0 truncate text-sm font-medium">{entry.name}</p>
|
||||
<Badge variant="outline" className="h-5 px-1.5 text-[10px]">
|
||||
{SUPPORT_SCOPE_LABELS[entry.scope]}
|
||||
{scopeLabels[entry.scope]}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
|
||||
@@ -135,12 +135,16 @@ export function useUpdateAccountContext() {
|
||||
}) => api.accounts.updateContext(name, { context_mode, context_group, continuity_mode }),
|
||||
onSuccess: (_data, vars) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['accounts'] });
|
||||
const normalizedGroup = (vars.context_group || 'default')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-');
|
||||
const contextSummary =
|
||||
vars.context_mode === 'shared'
|
||||
? vars.continuity_mode === 'deeper'
|
||||
? `shared (${(vars.context_group || 'default').trim().toLowerCase().replace(/\s+/g, '-')}, deeper continuity)`
|
||||
: `shared (${(vars.context_group || 'default').trim().toLowerCase().replace(/\s+/g, '-')}, standard)`
|
||||
: 'isolated';
|
||||
? `${t('accountsPage.sharedDeeper')} (${normalizedGroup})`
|
||||
: `${t('accountsPage.sharedStandard')} (${normalizedGroup})`
|
||||
: t('accountsPage.isolated');
|
||||
toast.success(t('toasts.contextUpdated', { name: vars.name, summary: contextSummary }));
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
@@ -180,18 +184,14 @@ export function useConfirmLegacyAccountPolicies() {
|
||||
onSuccess: ({ updatedCount, failedCount }) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['accounts'] });
|
||||
if (failedCount > 0 && updatedCount > 0) {
|
||||
// TODO i18n: missing key for partial legacy confirmation with failures
|
||||
toast.error(
|
||||
`Confirmed ${updatedCount} legacy account${updatedCount > 1 ? 's' : ''}, but ${failedCount} update${failedCount > 1 ? 's' : ''} failed. Refreshed account state.`
|
||||
t('toasts.legacyConfirmPartial', { updated: updatedCount, failed: failedCount })
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (failedCount > 0) {
|
||||
// TODO i18n: missing key for all legacy confirmations failed
|
||||
toast.error(
|
||||
`Failed to confirm ${failedCount} legacy account${failedCount > 1 ? 's' : ''}. Refreshed account state.`
|
||||
);
|
||||
toast.error(t('toasts.legacyConfirmAllFailed', { failed: failedCount }));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -131,8 +131,7 @@ export function useCliproxyAuthFlow() {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
isAuthenticating: false,
|
||||
// TODO i18n: missing key for 'Authentication timed out. Please try again.'
|
||||
error: 'Authentication timed out. Please try again.',
|
||||
error: t('toasts.providerAuthTimeout'),
|
||||
}));
|
||||
}
|
||||
return;
|
||||
@@ -161,8 +160,7 @@ export function useCliproxyAuthFlow() {
|
||||
const hasAccount = typeof data.account === 'object' && data.account !== null;
|
||||
if (!hasAccount) {
|
||||
stopPolling();
|
||||
// TODO i18n: missing key for 'Authenticated account could not be registered'
|
||||
const errorMsg = 'Authenticated account could not be registered';
|
||||
const errorMsg = t('toasts.providerAccountRegistrationFailed');
|
||||
toast.error(errorMsg);
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
@@ -226,11 +224,10 @@ export function useCliproxyAuthFlow() {
|
||||
}
|
||||
|
||||
stopPolling();
|
||||
// TODO i18n: missing key for 'Lost contact with the auth status endpoint'
|
||||
const message =
|
||||
error instanceof Error && error.message.trim().length > 0
|
||||
? error.message
|
||||
: 'Lost contact with the auth status endpoint';
|
||||
: t('toasts.providerLostStatusEndpoint');
|
||||
toast.error(message);
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
@@ -245,10 +242,9 @@ export function useCliproxyAuthFlow() {
|
||||
const startAuth = useCallback(
|
||||
async (provider: string, options?: StartAuthOptions) => {
|
||||
if (!isValidProvider(provider)) {
|
||||
// TODO i18n: missing key for 'Unknown provider: {{provider}}'
|
||||
setState({
|
||||
...INITIAL_STATE,
|
||||
error: `Unknown provider: ${provider}`,
|
||||
error: t('toasts.providerUnknown', { provider }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -319,13 +315,12 @@ export function useCliproxyAuthFlow() {
|
||||
openedAuthUrlRef.current = false;
|
||||
setState(INITIAL_STATE);
|
||||
} else {
|
||||
// TODO i18n: missing key for 'Authenticated account could not be registered' (start endpoint)
|
||||
const errorMsg =
|
||||
typeof data.error === 'string'
|
||||
? data.error
|
||||
: success
|
||||
? 'Authenticated account could not be registered'
|
||||
: 'Authentication failed';
|
||||
? t('toasts.providerAccountRegistrationFailed')
|
||||
: t('auth.loginFailed');
|
||||
toast.error(errorMsg);
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
@@ -370,8 +365,8 @@ export function useCliproxyAuthFlow() {
|
||||
const success = data.success === true;
|
||||
|
||||
if (!response.ok || !success) {
|
||||
// TODO i18n: missing key for 'Failed to start OAuth'
|
||||
const errorMsg = typeof data.error === 'string' ? data.error : 'Failed to start OAuth';
|
||||
const errorMsg =
|
||||
typeof data.error === 'string' ? data.error : t('toasts.providerStartOAuthFailed');
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
@@ -418,7 +413,7 @@ export function useCliproxyAuthFlow() {
|
||||
setState(INITIAL_STATE);
|
||||
return;
|
||||
}
|
||||
const message = error instanceof Error ? error.message : 'Authentication failed';
|
||||
const message = error instanceof Error ? error.message : t('auth.loginFailed');
|
||||
toast.error(message);
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
@@ -427,7 +422,7 @@ export function useCliproxyAuthFlow() {
|
||||
}));
|
||||
}
|
||||
},
|
||||
[isActiveAttempt, pollStatus, stopPolling, queryClient]
|
||||
[isActiveAttempt, pollStatus, stopPolling, queryClient, t]
|
||||
);
|
||||
|
||||
const cancelAuth = useCallback(() => {
|
||||
@@ -486,21 +481,20 @@ export function useCliproxyAuthFlow() {
|
||||
toast.success(t('toasts.providerAuthSuccess', { provider: currentProvider }));
|
||||
setState(INITIAL_STATE);
|
||||
} else {
|
||||
// TODO i18n: missing key for 'Callback submission failed'
|
||||
const errorMsg =
|
||||
typeof data.error === 'string'
|
||||
? data.error
|
||||
: success
|
||||
? 'Authenticated account could not be registered'
|
||||
: 'Callback submission failed';
|
||||
? t('toasts.providerAccountRegistrationFailed')
|
||||
: t('toasts.providerCallbackFailed');
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!isActiveAttempt(attemptId)) {
|
||||
return;
|
||||
}
|
||||
// TODO i18n: missing key for 'Failed to submit callback'
|
||||
const message = error instanceof Error ? error.message : 'Failed to submit callback';
|
||||
const message =
|
||||
error instanceof Error ? error.message : t('toasts.providerSubmitCallbackFailed');
|
||||
toast.error(message);
|
||||
setState((prev) => ({ ...prev, isSubmittingCallback: false, error: message }));
|
||||
}
|
||||
|
||||
+72
-43
@@ -1,3 +1,5 @@
|
||||
import i18n from '@/lib/i18n';
|
||||
|
||||
export interface CodexTopLevelSettingsView {
|
||||
model: string | null;
|
||||
modelReasoningEffort: string | null;
|
||||
@@ -58,46 +60,73 @@ base_url = "http://127.0.0.1:8317/api/provider/codex"
|
||||
env_key = "CLIPROXY_API_KEY"
|
||||
wire_api = "responses"`;
|
||||
|
||||
export const KNOWN_CODEX_FEATURES: CodexFeatureCatalogEntry[] = [
|
||||
{
|
||||
name: 'multi_agent',
|
||||
label: 'Multi-agent', // TODO i18n: missing key for codex feature
|
||||
description: 'Enable subagent collaboration tools.', // TODO i18n: missing key
|
||||
},
|
||||
{
|
||||
name: 'unified_exec',
|
||||
label: 'Unified exec', // TODO i18n: missing key for codex feature
|
||||
description: 'Use the PTY-backed unified exec tool.', // TODO i18n: missing key
|
||||
},
|
||||
{
|
||||
name: 'shell_snapshot',
|
||||
label: 'Shell snapshot', // TODO i18n: missing key for codex feature
|
||||
description: 'Reuse shell environment snapshots.', // TODO i18n: missing key
|
||||
},
|
||||
{
|
||||
name: 'apply_patch_freeform',
|
||||
label: 'Apply patch', // TODO i18n: missing key for codex feature
|
||||
description: 'Enable freeform apply_patch edits.', // TODO i18n: missing key
|
||||
},
|
||||
{ name: 'js_repl', label: 'JS REPL', description: 'Enable the Node-backed JavaScript REPL.' }, // TODO i18n: missing keys
|
||||
{
|
||||
name: 'runtime_metrics',
|
||||
label: 'Runtime metrics', // TODO i18n: missing key for codex feature
|
||||
description: 'Collect Codex runtime metrics.', // TODO i18n: missing key
|
||||
},
|
||||
{
|
||||
name: 'prevent_idle_sleep',
|
||||
label: 'Prevent idle sleep', // TODO i18n: missing key for codex feature
|
||||
description: 'Keep the machine awake while active.', // TODO i18n: missing key
|
||||
},
|
||||
{ name: 'fast_mode', label: 'Fast mode', description: 'Allow the fast service tier path.' }, // TODO i18n: missing keys
|
||||
{ name: 'apps', label: 'Apps', description: 'Enable ChatGPT Apps and connectors support.' }, // TODO i18n: missing keys
|
||||
{
|
||||
name: 'smart_approvals',
|
||||
label: 'Smart approvals', // TODO i18n: missing key for codex feature
|
||||
description: 'Route eligible approvals through the guardian flow.', // TODO i18n: missing key
|
||||
},
|
||||
];
|
||||
export const KNOWN_CODEX_FEATURE_NAMES = [
|
||||
'multi_agent',
|
||||
'unified_exec',
|
||||
'shell_snapshot',
|
||||
'apply_patch_freeform',
|
||||
'js_repl',
|
||||
'runtime_metrics',
|
||||
'prevent_idle_sleep',
|
||||
'fast_mode',
|
||||
'apps',
|
||||
'smart_approvals',
|
||||
] as const;
|
||||
|
||||
export function getKnownCodexFeatures(): CodexFeatureCatalogEntry[] {
|
||||
return [
|
||||
{
|
||||
name: 'multi_agent',
|
||||
label: i18n.t('codex.featureMultiAgentLabel'),
|
||||
description: i18n.t('codex.featureMultiAgentDesc'),
|
||||
},
|
||||
{
|
||||
name: 'unified_exec',
|
||||
label: i18n.t('codex.featureUnifiedExecLabel'),
|
||||
description: i18n.t('codex.featureUnifiedExecDesc'),
|
||||
},
|
||||
{
|
||||
name: 'shell_snapshot',
|
||||
label: i18n.t('codex.featureShellSnapshotLabel'),
|
||||
description: i18n.t('codex.featureShellSnapshotDesc'),
|
||||
},
|
||||
{
|
||||
name: 'apply_patch_freeform',
|
||||
label: i18n.t('codex.featureApplyPatchLabel'),
|
||||
description: i18n.t('codex.featureApplyPatchDesc'),
|
||||
},
|
||||
{
|
||||
name: 'js_repl',
|
||||
label: i18n.t('codex.featureJsReplLabel'),
|
||||
description: i18n.t('codex.featureJsReplDesc'),
|
||||
},
|
||||
{
|
||||
name: 'runtime_metrics',
|
||||
label: i18n.t('codex.featureRuntimeMetricsLabel'),
|
||||
description: i18n.t('codex.featureRuntimeMetricsDesc'),
|
||||
},
|
||||
{
|
||||
name: 'prevent_idle_sleep',
|
||||
label: i18n.t('codex.featurePreventIdleSleepLabel'),
|
||||
description: i18n.t('codex.featurePreventIdleSleepDesc'),
|
||||
},
|
||||
{
|
||||
name: 'fast_mode',
|
||||
label: i18n.t('codex.featureFastModeLabel'),
|
||||
description: i18n.t('codex.featureFastModeDesc'),
|
||||
},
|
||||
{
|
||||
name: 'apps',
|
||||
label: i18n.t('codex.featureAppsLabel'),
|
||||
description: i18n.t('codex.featureAppsDesc'),
|
||||
},
|
||||
{
|
||||
name: 'smart_approvals',
|
||||
label: i18n.t('codex.featureSmartApprovalsLabel'),
|
||||
description: i18n.t('codex.featureSmartApprovalsDesc'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function asObject(value: unknown): Record<string, unknown> | null {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
@@ -222,9 +251,9 @@ export function readCodexFeatureState(
|
||||
const features = asObject(config?.features);
|
||||
const state: Record<string, boolean | null> = {};
|
||||
|
||||
for (const feature of KNOWN_CODEX_FEATURES) {
|
||||
const value = features?.[feature.name];
|
||||
state[feature.name] = typeof value === 'boolean' ? value : null;
|
||||
for (const featureName of KNOWN_CODEX_FEATURE_NAMES) {
|
||||
const value = features?.[featureName];
|
||||
state[featureName] = typeof value === 'boolean' ? value : null;
|
||||
}
|
||||
|
||||
if (features) {
|
||||
|
||||
@@ -1066,6 +1066,8 @@ const resources = {
|
||||
},
|
||||
apiProfiles: {
|
||||
title: 'Profiles',
|
||||
sidebarTitle: 'API Profiles',
|
||||
sidebarSubtitle: 'Premium APIs, local runtimes, custom endpoints',
|
||||
new: 'New',
|
||||
searchPlaceholder: 'Search profiles...',
|
||||
loadingProfiles: 'Loading profiles...',
|
||||
@@ -1087,6 +1089,19 @@ const resources = {
|
||||
unsavedChangesDesc:
|
||||
'You have unsaved changes in "{{current}}". Discard and switch to "{{next}}"?',
|
||||
discardSwitch: 'Discard and Switch',
|
||||
noOrphansFound: 'No orphan profile settings found',
|
||||
confirmRegisterOrphans:
|
||||
'Found {{total}} orphan profile setting file(s), with {{valid}} ready to register. Register them now?',
|
||||
registeredWithSkipped: ' ({{count}} skipped)',
|
||||
registeredProfiles: 'Registered {{count}} profile(s)',
|
||||
copyPrompt: 'Copy "{{name}}" to which new profile name?',
|
||||
destinationEmpty: 'Destination profile name cannot be empty',
|
||||
exportRedacted:
|
||||
'Export created with redacted token. Use include-secrets flow in CLI if needed.',
|
||||
exportDownloaded: 'Profile export downloaded',
|
||||
importFailed: 'Failed to import profile bundle',
|
||||
discoverOrphans: 'Discover orphan profiles',
|
||||
importProfileBundle: 'Import profile bundle',
|
||||
},
|
||||
accountsPage: {
|
||||
title: 'Accounts',
|
||||
@@ -1578,6 +1593,11 @@ const resources = {
|
||||
fiveHourResets: '5h resets {{time}}',
|
||||
weeklyResets: 'Weekly resets {{time}}',
|
||||
},
|
||||
utils: {
|
||||
codeReview5h: 'Code review (5h)',
|
||||
codeReviewWeekly: 'Code review (weekly)',
|
||||
codeReview: 'Code review',
|
||||
},
|
||||
sponsorButton: {
|
||||
title: 'Sponsor this project on GitHub',
|
||||
sponsor: 'Sponsor',
|
||||
@@ -1635,6 +1655,8 @@ const resources = {
|
||||
conflictDesc:
|
||||
'This settings file was modified by another process. Overwrite with your changes or discard?',
|
||||
overwrite: 'Overwrite',
|
||||
invalidJson: 'Invalid JSON',
|
||||
failedSave: 'Failed to save',
|
||||
},
|
||||
|
||||
// ========================================
|
||||
@@ -1852,12 +1874,60 @@ const resources = {
|
||||
nativeCodexRuntime: 'Native Codex Runtime',
|
||||
ccsCodexProvider: 'CCS Codex provider / bridge',
|
||||
codexDocs: 'Codex docs',
|
||||
howCodexWorks: 'How Codex works in CCS',
|
||||
supportedFlows: 'Supported flows',
|
||||
twoSupportedPaths: 'Two supported paths:',
|
||||
nativeLabel: 'Native:',
|
||||
nativeDesc: 'Codex is a first-class, runtime-only target in CCS v1.',
|
||||
nativeConfigLabel: 'Native config:',
|
||||
nativeConfigDesc: 'ccs-codex and ccsx launch native Codex using your saved defaults.',
|
||||
transientOverridesLabel: 'Transient overrides:',
|
||||
transientOverridesDesc:
|
||||
'ccsxp (or ccs codex --target codex) uses the CCS provider shortcut.',
|
||||
cliproxyDefaultLabel: 'CLIProxy default:',
|
||||
cliproxyDefaultDesc:
|
||||
'To make plain codex use CLIProxy, set model_provider = "cliproxy" and add the recipe below.',
|
||||
ccsBridge: 'CCS Bridge',
|
||||
apiProfilesDefault: 'API profiles continue to default to Claude or Droid.',
|
||||
runtimeInstall: 'Runtime install',
|
||||
detected: 'Detected',
|
||||
notFound: 'Not found',
|
||||
detectionSource: 'Detection source',
|
||||
binaryPath: 'Binary path',
|
||||
installDirectory: 'Install directory',
|
||||
versionLabel: 'Version',
|
||||
nativeAliases: 'Native aliases',
|
||||
ccsProviderShortcut: 'CCS provider shortcut',
|
||||
configOverrideSupport: '--config override support',
|
||||
available: 'Available',
|
||||
missing: 'Missing',
|
||||
cliproxyNativeCodex: 'CLIProxy-backed native Codex',
|
||||
builtInLabel: 'Built-in:',
|
||||
builtInCcsxpDesc: 'Use ccsxp for the CCS provider shortcut.',
|
||||
nativeRecipeLabel: 'Native:',
|
||||
nativeRecipeDesc: 'Configure the recipe below to use CLIProxy directly with codex.',
|
||||
saveProviderNamedCliproxy:
|
||||
'Save a provider named cliproxy with the base URL and env key above.',
|
||||
inTopLevelSetDefault: 'In Top-level settings, set Default provider to cliproxy.',
|
||||
exportCliproxyApiKey:
|
||||
'Export CLIPROXY_API_KEY in your shell before launching native Codex.',
|
||||
noConfigOverrides:
|
||||
'This Codex build can still use the native path, but CCS-backed Codex routing via ccsxp or ccs codex --target codex stays unavailable until the detected Codex binary exposes --config overrides.',
|
||||
configFile: 'Config file',
|
||||
path: 'Path',
|
||||
resolved: 'Resolved',
|
||||
size: 'Size',
|
||||
lastModified: 'Last modified',
|
||||
tomlWarning: 'TOML warning',
|
||||
readWarning: 'Read warning',
|
||||
currentUserLayerSummary: 'Current user-layer summary',
|
||||
activeProfile: 'Active profile',
|
||||
notSet: 'Not set',
|
||||
providersCount: 'providers: {{count}}',
|
||||
profilesCount: 'profiles: {{count}}',
|
||||
enabledFeaturesCount: 'enabled features: {{count}}',
|
||||
mcpServersCount: 'mcp servers: {{count}}',
|
||||
userLayerKeysPresent: 'User-layer keys present',
|
||||
recommendedSetupFlow: 'Recommended setup flow',
|
||||
fastestPath: 'Fastest path',
|
||||
officialChannels: 'Official Channels',
|
||||
@@ -1866,6 +1936,9 @@ const resources = {
|
||||
runBuiltInCodex: 'Run built-in Codex on Codex',
|
||||
runBuiltInCodexExplicit: 'Run built-in Codex on Codex (explicit)',
|
||||
openCodexDashboard: 'Open Codex dashboard',
|
||||
nativeShortAlias: 'Native short alias',
|
||||
ccsCodexShortcut: 'CCS Codex shortcut',
|
||||
explicitProviderRoute: 'Explicit provider route',
|
||||
status: 'Status',
|
||||
profiles: 'Profiles',
|
||||
createNewProfile: 'Create new profile',
|
||||
@@ -1895,6 +1968,10 @@ const resources = {
|
||||
untrusted: 'untrusted',
|
||||
noProjectTrustEntries: 'No explicit project trust entries saved.',
|
||||
codexNativeRecipe: 'Saved native Codex recipe',
|
||||
runtimeVsProvider: 'Runtime vs provider',
|
||||
honorsSavedNativeConfig: 'Honors saved native user config',
|
||||
usesTransientOverrides: 'Uses transient overrides',
|
||||
unavailableNoConfig: 'Unavailable (Codex build lacks --config support).',
|
||||
gptContextCap: 'GPT-5.4 context cap',
|
||||
usageLimitCost: 'Usage-limit cost above 272K',
|
||||
longContextOverride: 'Long context override',
|
||||
@@ -1911,6 +1988,37 @@ const resources = {
|
||||
configYaml: 'config.yaml',
|
||||
flow: 'Flow',
|
||||
docsTab: 'Docs',
|
||||
yes: 'Yes',
|
||||
no: 'No',
|
||||
warningsTitle: 'Warnings',
|
||||
features: 'Features',
|
||||
featuresDesc: 'Toggle the supported Codex feature flags CCS can safely manage.',
|
||||
configOnlyFlags: 'Existing config-only flags',
|
||||
configOnlyFlagsDesc:
|
||||
'These feature keys already exist in your `config.toml`, so CCS can surface them without claiming full catalog coverage.',
|
||||
existing: 'existing',
|
||||
nonBooleanForm: 'Stored in a non-boolean form. Use raw TOML if you need to edit it.',
|
||||
discoveredFromFile: "Discovered from the current file instead of CCS's built-in catalog.",
|
||||
featureMultiAgentLabel: 'Multi-agent',
|
||||
featureMultiAgentDesc: 'Enable subagent collaboration tools.',
|
||||
featureUnifiedExecLabel: 'Unified exec',
|
||||
featureUnifiedExecDesc: 'Use the PTY-backed unified exec tool.',
|
||||
featureShellSnapshotLabel: 'Shell snapshot',
|
||||
featureShellSnapshotDesc: 'Reuse shell environment snapshots.',
|
||||
featureApplyPatchLabel: 'Apply patch',
|
||||
featureApplyPatchDesc: 'Enable freeform apply_patch edits.',
|
||||
featureJsReplLabel: 'JS REPL',
|
||||
featureJsReplDesc: 'Enable the Node-backed JavaScript REPL.',
|
||||
featureRuntimeMetricsLabel: 'Runtime metrics',
|
||||
featureRuntimeMetricsDesc: 'Collect Codex runtime metrics.',
|
||||
featurePreventIdleSleepLabel: 'Prevent idle sleep',
|
||||
featurePreventIdleSleepDesc: 'Keep the machine awake while active.',
|
||||
featureFastModeLabel: 'Fast mode',
|
||||
featureFastModeDesc: 'Allow the fast service tier path.',
|
||||
featureAppsLabel: 'Apps',
|
||||
featureAppsDesc: 'Enable ChatGPT Apps and connectors support.',
|
||||
featureSmartApprovalsLabel: 'Smart approvals',
|
||||
featureSmartApprovalsDesc: 'Route eligible approvals through the guardian flow.',
|
||||
},
|
||||
droidSettings: {
|
||||
quickControls: 'Quick Controls',
|
||||
@@ -2022,6 +2130,13 @@ const resources = {
|
||||
syncFailed: 'Sync failed: {{error}}',
|
||||
providerAuthSuccess: '{{provider}} authentication successful',
|
||||
providerDeviceCodeInCallback: 'Provider returned Device Code flow in callback mode',
|
||||
providerAuthTimeout: 'Authentication timed out. Please try again.',
|
||||
providerAccountRegistrationFailed: 'Authenticated account could not be registered',
|
||||
providerLostStatusEndpoint: 'Lost contact with the auth status endpoint',
|
||||
providerUnknown: 'Unknown provider: {{provider}}',
|
||||
providerStartOAuthFailed: 'Failed to start OAuth',
|
||||
providerCallbackFailed: 'Callback submission failed',
|
||||
providerSubmitCallbackFailed: 'Failed to submit callback',
|
||||
loggingConfigSaved: 'Logging configuration saved.',
|
||||
loggingConfigSaveFailed: 'Failed to save logging configuration.',
|
||||
unifiedConfigUpdated: 'Configuration updated successfully',
|
||||
@@ -2039,6 +2154,10 @@ const resources = {
|
||||
legacyConfirmFailed: 'Legacy account "{{name}}" failed confirmation: {{error}}',
|
||||
legacyConfirmSuccess_one: '{{count}} legacy account confirmed',
|
||||
legacyConfirmSuccess_other: '{{count}} legacy accounts confirmed',
|
||||
legacyConfirmPartial:
|
||||
'Confirmed {{updated}} legacy account(s), but {{failed}} update(s) failed. Refreshed account state.',
|
||||
legacyConfirmAllFailed:
|
||||
'Failed to confirm {{failed}} legacy account(s). Refreshed account state.',
|
||||
noLegacyAccounts: 'No legacy accounts need confirmation',
|
||||
routingStrategySet: 'Routing strategy set to {{strategy}}',
|
||||
variantCreated: 'Variant created successfully',
|
||||
@@ -3315,6 +3434,8 @@ const resources = {
|
||||
},
|
||||
apiProfiles: {
|
||||
title: 'API 配置',
|
||||
sidebarTitle: 'API 配置',
|
||||
sidebarSubtitle: '高级 API、本地运行时、自定义端点',
|
||||
new: '新建',
|
||||
searchPlaceholder: '搜索配置...',
|
||||
loadingProfiles: '正在加载配置...',
|
||||
@@ -3334,6 +3455,19 @@ const resources = {
|
||||
unsavedChangesTitle: '未保存的更改',
|
||||
unsavedChangesDesc: '你在“{{current}}”中有未保存内容,是否丢弃并切换到“{{next}}”?',
|
||||
discardSwitch: '丢弃并切换',
|
||||
noOrphansFound: '未发现孤立配置',
|
||||
confirmRegisterOrphans:
|
||||
'发现 {{total}} 个孤立配置文件,其中 {{valid}} 个可直接注册。现在注册吗?',
|
||||
registeredWithSkipped: '(跳过 {{count}} 个)',
|
||||
registeredProfiles: '已注册 {{count}} 个配置',
|
||||
copyPrompt: '将“{{name}}”复制为哪个新配置名称?',
|
||||
destinationEmpty: '目标配置名称不能为空',
|
||||
exportRedacted:
|
||||
'导出内容中的令牌已脱敏。如需包含密钥,请改用 CLI 的 include-secrets 流程。',
|
||||
exportDownloaded: '配置导出已下载',
|
||||
importFailed: '导入配置包失败',
|
||||
discoverOrphans: '发现孤立配置',
|
||||
importProfileBundle: '导入配置包',
|
||||
},
|
||||
accountsPage: {
|
||||
title: '账号',
|
||||
@@ -3812,6 +3946,11 @@ const resources = {
|
||||
fiveHourResets: '5 小时重置于 {{time}}',
|
||||
weeklyResets: '每周重置于 {{time}}',
|
||||
},
|
||||
utils: {
|
||||
codeReview5h: '代码审查(5 小时)',
|
||||
codeReviewWeekly: '代码审查(每周)',
|
||||
codeReview: '代码审查',
|
||||
},
|
||||
sponsorButton: {
|
||||
title: '在 GitHub 上赞助此项目',
|
||||
sponsor: '赞助',
|
||||
@@ -3868,6 +4007,8 @@ const resources = {
|
||||
conflictTitle: '文件被外部修改',
|
||||
conflictDesc: '此设置文件已被其他进程修改。用你的更改覆盖还是丢弃?',
|
||||
overwrite: '覆盖',
|
||||
invalidJson: 'JSON 无效',
|
||||
failedSave: '保存失败',
|
||||
},
|
||||
setupWizard: {
|
||||
title: '快速设置向导',
|
||||
@@ -4131,6 +4272,90 @@ const resources = {
|
||||
configYaml: 'config.yaml',
|
||||
flow: '工作流',
|
||||
docsTab: '文档',
|
||||
features: '功能',
|
||||
featuresDesc: '切换 CCS 可安全管理的 Codex 功能开关。',
|
||||
configOnlyFlags: '仅配置文件中的现有开关',
|
||||
configOnlyFlagsDesc:
|
||||
'这些功能键已存在于你的 `config.toml` 中,因此 CCS 可以显示它们,而不必宣称自己覆盖了完整目录。',
|
||||
existing: '已存在',
|
||||
nonBooleanForm: '该值以非布尔形式保存。如果需要编辑,请使用原始 TOML。',
|
||||
discoveredFromFile: '这是从当前文件中发现的,而不是来自 CCS 内置目录。',
|
||||
featureMultiAgentLabel: 'Multi-agent',
|
||||
featureMultiAgentDesc: '启用子代理协作工具。',
|
||||
featureUnifiedExecLabel: 'Unified exec',
|
||||
featureUnifiedExecDesc: '使用基于 PTY 的统一 exec 工具。',
|
||||
featureShellSnapshotLabel: 'Shell snapshot',
|
||||
featureShellSnapshotDesc: '复用 shell 环境快照。',
|
||||
featureApplyPatchLabel: 'Apply patch',
|
||||
featureApplyPatchDesc: '启用自由格式 apply_patch 编辑。',
|
||||
featureJsReplLabel: 'JS REPL',
|
||||
featureJsReplDesc: '启用基于 Node 的 JavaScript REPL。',
|
||||
featureRuntimeMetricsLabel: 'Runtime metrics',
|
||||
featureRuntimeMetricsDesc: '收集 Codex 运行时指标。',
|
||||
featurePreventIdleSleepLabel: 'Prevent idle sleep',
|
||||
featurePreventIdleSleepDesc: '保持机器在运行期间不休眠。',
|
||||
featureFastModeLabel: 'Fast mode',
|
||||
featureFastModeDesc: '允许使用快速服务层路径。',
|
||||
featureAppsLabel: 'Apps',
|
||||
featureAppsDesc: '启用 ChatGPT Apps 和连接器支持。',
|
||||
featureSmartApprovalsLabel: 'Smart approvals',
|
||||
featureSmartApprovalsDesc: '将符合条件的审批交给 guardian 流程处理。',
|
||||
howCodexWorks: 'Codex 在 CCS 中的工作方式',
|
||||
nativeConfigLabel: '原生配置:',
|
||||
nativeConfigDesc: 'ccs-codex 和 ccsx 会使用你保存的默认值启动原生 Codex。',
|
||||
transientOverridesLabel: '临时覆盖:',
|
||||
transientOverridesDesc: 'ccsxp(或 ccs codex --target codex)会使用 CCS 提供商快捷方式。',
|
||||
cliproxyDefaultLabel: 'CLIProxy 默认:',
|
||||
cliproxyDefaultDesc:
|
||||
'如果你想让普通的 codex 走 CLIProxy,请设置 model_provider = "cliproxy",并加入下面的配方。',
|
||||
runtimeInstall: '运行时安装',
|
||||
detected: '已检测到',
|
||||
notFound: '未找到',
|
||||
detectionSource: '检测来源',
|
||||
binaryPath: '二进制路径',
|
||||
installDirectory: '安装目录',
|
||||
versionLabel: '版本',
|
||||
nativeAliases: '原生别名',
|
||||
ccsProviderShortcut: 'CCS 提供商快捷方式',
|
||||
configOverrideSupport: '--config 覆盖支持',
|
||||
available: '可用',
|
||||
missing: '缺失',
|
||||
cliproxyNativeCodex: '由 CLIProxy 支持的原生 Codex',
|
||||
builtInLabel: '内置:',
|
||||
builtInCcsxpDesc: '使用 ccsxp 作为 CCS 提供商快捷方式。',
|
||||
nativeRecipeLabel: '原生:',
|
||||
nativeRecipeDesc: '按下面的配方配置,即可让 codex 直接走 CLIProxy。',
|
||||
saveProviderNamedCliproxy:
|
||||
'保存一个名为 cliproxy 的 provider,并填入上面的 base URL 与 env key。',
|
||||
inTopLevelSetDefault: '在顶层设置中,将 Default provider 设为 cliproxy。',
|
||||
exportCliproxyApiKey: '在启动原生 Codex 之前,在 shell 中导出 CLIPROXY_API_KEY。',
|
||||
noConfigOverrides:
|
||||
'这个 Codex 版本仍然可以走原生路径,但在检测到的二进制支持 --config 覆盖之前,ccsxp 或 ccs codex --target codex 这样的 CCS 路由不可用。',
|
||||
configFile: '配置文件',
|
||||
path: '路径',
|
||||
resolved: '已解析',
|
||||
size: '大小',
|
||||
lastModified: '最后修改',
|
||||
tomlWarning: 'TOML 警告',
|
||||
readWarning: '读取警告',
|
||||
currentUserLayerSummary: '当前用户层摘要',
|
||||
activeProfile: '当前激活的配置',
|
||||
notSet: '未设置',
|
||||
providersCount: 'providers: {{count}}',
|
||||
profilesCount: 'profiles: {{count}}',
|
||||
enabledFeaturesCount: 'enabled features: {{count}}',
|
||||
mcpServersCount: 'mcp servers: {{count}}',
|
||||
userLayerKeysPresent: '已存在的用户层键',
|
||||
nativeShortAlias: '原生短别名',
|
||||
ccsCodexShortcut: 'CCS Codex 快捷方式',
|
||||
explicitProviderRoute: '显式 provider 路径',
|
||||
runtimeVsProvider: '运行时与 provider 的区别',
|
||||
honorsSavedNativeConfig: '遵循已保存的原生用户配置',
|
||||
usesTransientOverrides: '使用临时覆盖',
|
||||
unavailableNoConfig: '不可用(当前 Codex 版本缺少 --config 支持)。',
|
||||
yes: '是',
|
||||
no: '否',
|
||||
warningsTitle: '警告',
|
||||
},
|
||||
droidSettings: {
|
||||
quickControls: '快捷控制',
|
||||
@@ -4234,6 +4459,13 @@ const resources = {
|
||||
syncFailed: '同步失败:{{error}}',
|
||||
providerAuthSuccess: '{{provider}} 认证成功',
|
||||
providerDeviceCodeInCallback: '提供商在回调模式中返回了设备码流程',
|
||||
providerAuthTimeout: '认证超时,请重试。',
|
||||
providerAccountRegistrationFailed: '已认证账号无法注册',
|
||||
providerLostStatusEndpoint: '与认证状态端点失去连接',
|
||||
providerUnknown: '未知提供商:{{provider}}',
|
||||
providerStartOAuthFailed: '启动 OAuth 失败',
|
||||
providerCallbackFailed: '回调提交失败',
|
||||
providerSubmitCallbackFailed: '提交回调失败',
|
||||
loggingConfigSaved: '日志配置已保存。',
|
||||
loggingConfigSaveFailed: '保存日志配置失败。',
|
||||
unifiedConfigUpdated: '配置更新成功',
|
||||
@@ -4250,6 +4482,9 @@ const resources = {
|
||||
legacyConfirmFailed: '旧版账号「{{name}}」确认失败:{{error}}',
|
||||
legacyConfirmSuccess_one: '{{count}} 个旧版账号已确认',
|
||||
legacyConfirmSuccess_other: '{{count}} 个旧版账号已确认',
|
||||
legacyConfirmPartial:
|
||||
'已确认 {{updated}} 个旧版账号,但有 {{failed}} 个更新失败。账号状态已刷新。',
|
||||
legacyConfirmAllFailed: '确认 {{failed}} 个旧版账号失败。账号状态已刷新。',
|
||||
noLegacyAccounts: '没有需要确认的旧版账号',
|
||||
routingStrategySet: '路由策略已设为 {{strategy}}',
|
||||
variantCreated: '变体创建成功',
|
||||
@@ -5604,6 +5839,8 @@ const resources = {
|
||||
},
|
||||
apiProfiles: {
|
||||
title: 'Hồ sơ API',
|
||||
sidebarTitle: 'Hồ sơ API',
|
||||
sidebarSubtitle: 'API cao cấp, runtime cục bộ, endpoint tùy chỉnh',
|
||||
new: 'Mới',
|
||||
searchPlaceholder: 'Tìm kiếm hồ sơ...',
|
||||
loadingProfiles: 'Đang tải hồ sơ...',
|
||||
@@ -5625,6 +5862,18 @@ const resources = {
|
||||
unsavedChangesDesc:
|
||||
'Bạn có những thay đổi chưa được lưu trong "{{current}}". Hủy và chuyển sang "{{next}}"?',
|
||||
discardSwitch: 'Loại bỏ và chuyển đổi',
|
||||
noOrphansFound: 'Không tìm thấy cấu hình hồ sơ mồ côi',
|
||||
confirmRegisterOrphans:
|
||||
'Đã tìm thấy {{total}} tệp cấu hình hồ sơ mồ côi, trong đó {{valid}} tệp sẵn sàng đăng ký. Đăng ký ngay?',
|
||||
registeredWithSkipped: ' (bỏ qua {{count}})',
|
||||
registeredProfiles: 'Đã đăng ký {{count}} hồ sơ',
|
||||
copyPrompt: 'Sao chép "{{name}}" sang tên hồ sơ mới nào?',
|
||||
destinationEmpty: 'Tên hồ sơ đích không được để trống',
|
||||
exportRedacted: 'Bản xuất đã được che token. Dùng luồng include-secrets trong CLI nếu cần.',
|
||||
exportDownloaded: 'Đã tải xuống bản xuất hồ sơ',
|
||||
importFailed: 'Không nhập được gói hồ sơ',
|
||||
discoverOrphans: 'Phát hiện hồ sơ mồ côi',
|
||||
importProfileBundle: 'Nhập gói hồ sơ',
|
||||
},
|
||||
accountsPage: {
|
||||
title: 'Tài khoản',
|
||||
@@ -6116,6 +6365,11 @@ const resources = {
|
||||
fiveHourResets: 'Reset 5h lúc {{time}}',
|
||||
weeklyResets: 'Reset hàng tuần lúc {{time}}',
|
||||
},
|
||||
utils: {
|
||||
codeReview5h: 'Đánh giá mã (5h)',
|
||||
codeReviewWeekly: 'Đánh giá mã (hàng tuần)',
|
||||
codeReview: 'Đánh giá mã',
|
||||
},
|
||||
sponsorButton: {
|
||||
title: 'Tài trợ dự án này trên GitHub',
|
||||
sponsor: 'Tài trợ',
|
||||
@@ -6173,6 +6427,8 @@ const resources = {
|
||||
conflictDesc:
|
||||
'Tệp cài đặt này đã bị thay đổi bởi một tiến trình khác. Ghi đè thay đổi của bạn hay hủy?',
|
||||
overwrite: 'Ghi đè',
|
||||
invalidJson: 'JSON không hợp lệ',
|
||||
failedSave: 'Không lưu được',
|
||||
},
|
||||
setupWizard: {
|
||||
title: 'Trình thiết lập nhanh',
|
||||
@@ -6438,6 +6694,91 @@ const resources = {
|
||||
configYaml: 'config.yaml',
|
||||
flow: 'Luồng',
|
||||
docsTab: 'Tài liệu',
|
||||
features: 'Tính năng',
|
||||
featuresDesc: 'Bật/tắt các cờ tính năng Codex mà CCS có thể quản lý an toàn.',
|
||||
configOnlyFlags: 'Cờ chỉ có trong cấu hình',
|
||||
configOnlyFlagsDesc:
|
||||
'Các khóa này đã tồn tại trong `config.toml`, nên CCS có thể hiển thị chúng mà không cần tuyên bố bao phủ toàn bộ danh mục.',
|
||||
existing: 'đã có',
|
||||
nonBooleanForm:
|
||||
'Giá trị đang được lưu ở dạng không phải boolean. Hãy dùng TOML thô nếu bạn muốn chỉnh sửa.',
|
||||
discoveredFromFile: 'Được phát hiện từ tệp hiện tại thay vì từ danh mục tích hợp của CCS.',
|
||||
featureMultiAgentLabel: 'Multi-agent',
|
||||
featureMultiAgentDesc: 'Bật các công cụ cộng tác giữa subagent.',
|
||||
featureUnifiedExecLabel: 'Unified exec',
|
||||
featureUnifiedExecDesc: 'Dùng công cụ exec hợp nhất dựa trên PTY.',
|
||||
featureShellSnapshotLabel: 'Shell snapshot',
|
||||
featureShellSnapshotDesc: 'Tái sử dụng snapshot môi trường shell.',
|
||||
featureApplyPatchLabel: 'Apply patch',
|
||||
featureApplyPatchDesc: 'Bật chỉnh sửa apply_patch dạng tự do.',
|
||||
featureJsReplLabel: 'JS REPL',
|
||||
featureJsReplDesc: 'Bật JavaScript REPL dựa trên Node.',
|
||||
featureRuntimeMetricsLabel: 'Runtime metrics',
|
||||
featureRuntimeMetricsDesc: 'Thu thập chỉ số runtime của Codex.',
|
||||
featurePreventIdleSleepLabel: 'Prevent idle sleep',
|
||||
featurePreventIdleSleepDesc: 'Giữ máy không ngủ khi đang hoạt động.',
|
||||
featureFastModeLabel: 'Fast mode',
|
||||
featureFastModeDesc: 'Cho phép dùng đường dẫn fast service tier.',
|
||||
featureAppsLabel: 'Apps',
|
||||
featureAppsDesc: 'Bật hỗ trợ ChatGPT Apps và connectors.',
|
||||
featureSmartApprovalsLabel: 'Smart approvals',
|
||||
featureSmartApprovalsDesc: 'Đưa các phê duyệt phù hợp qua luồng guardian.',
|
||||
howCodexWorks: 'Codex hoạt động trong CCS như thế nào',
|
||||
nativeConfigLabel: 'Cấu hình gốc:',
|
||||
nativeConfigDesc: 'ccs-codex và ccsx khởi chạy Codex gốc bằng các mặc định bạn đã lưu.',
|
||||
transientOverridesLabel: 'Ghi đè tạm thời:',
|
||||
transientOverridesDesc:
|
||||
'ccsxp (hoặc ccs codex --target codex) dùng đường tắt nhà cung cấp CCS.',
|
||||
cliproxyDefaultLabel: 'Mặc định CLIProxy:',
|
||||
cliproxyDefaultDesc:
|
||||
'Để lệnh codex thông thường đi qua CLIProxy, hãy đặt model_provider = "cliproxy" và dùng công thức bên dưới.',
|
||||
runtimeInstall: 'Cài đặt runtime',
|
||||
detected: 'Đã phát hiện',
|
||||
notFound: 'Không tìm thấy',
|
||||
detectionSource: 'Nguồn phát hiện',
|
||||
binaryPath: 'Đường dẫn binary',
|
||||
installDirectory: 'Thư mục cài đặt',
|
||||
versionLabel: 'Phiên bản',
|
||||
nativeAliases: 'Bí danh gốc',
|
||||
ccsProviderShortcut: 'Lối tắt nhà cung cấp CCS',
|
||||
configOverrideSupport: 'Hỗ trợ ghi đè --config',
|
||||
available: 'Có sẵn',
|
||||
missing: 'Thiếu',
|
||||
cliproxyNativeCodex: 'Codex gốc dùng CLIProxy',
|
||||
builtInLabel: 'Tích hợp:',
|
||||
builtInCcsxpDesc: 'Dùng ccsxp cho lối tắt nhà cung cấp CCS.',
|
||||
nativeRecipeLabel: 'Gốc:',
|
||||
nativeRecipeDesc: 'Cấu hình công thức bên dưới để dùng CLIProxy trực tiếp với codex.',
|
||||
saveProviderNamedCliproxy: 'Lưu một provider tên cliproxy với base URL và env key ở trên.',
|
||||
inTopLevelSetDefault: 'Trong Top-level settings, đặt Default provider là cliproxy.',
|
||||
exportCliproxyApiKey: 'Xuất CLIPROXY_API_KEY trong shell trước khi chạy Codex gốc.',
|
||||
noConfigOverrides:
|
||||
'Bản Codex này vẫn có thể dùng đường gốc, nhưng định tuyến Codex qua CCS như ccsxp hoặc ccs codex --target codex sẽ không hoạt động cho đến khi binary hỗ trợ ghi đè --config.',
|
||||
configFile: 'Tệp cấu hình',
|
||||
path: 'Đường dẫn',
|
||||
resolved: 'Đã phân giải',
|
||||
size: 'Kích thước',
|
||||
lastModified: 'Sửa đổi lần cuối',
|
||||
tomlWarning: 'Cảnh báo TOML',
|
||||
readWarning: 'Cảnh báo đọc',
|
||||
currentUserLayerSummary: 'Tóm tắt lớp người dùng hiện tại',
|
||||
activeProfile: 'Hồ sơ đang dùng',
|
||||
notSet: 'Chưa đặt',
|
||||
providersCount: 'providers: {{count}}',
|
||||
profilesCount: 'profiles: {{count}}',
|
||||
enabledFeaturesCount: 'enabled features: {{count}}',
|
||||
mcpServersCount: 'mcp servers: {{count}}',
|
||||
userLayerKeysPresent: 'Các khóa lớp người dùng hiện có',
|
||||
nativeShortAlias: 'Bí danh gốc ngắn',
|
||||
ccsCodexShortcut: 'Lối tắt CCS Codex',
|
||||
explicitProviderRoute: 'Tuyến provider tường minh',
|
||||
runtimeVsProvider: 'Runtime so với provider',
|
||||
honorsSavedNativeConfig: 'Tuân theo cấu hình gốc đã lưu',
|
||||
usesTransientOverrides: 'Dùng ghi đè tạm thời',
|
||||
unavailableNoConfig: 'Không khả dụng (bản Codex này thiếu hỗ trợ --config).',
|
||||
yes: 'Có',
|
||||
no: 'Không',
|
||||
warningsTitle: 'Cảnh báo',
|
||||
},
|
||||
droidSettings: {
|
||||
quickControls: 'Điều khiển nhanh',
|
||||
@@ -6541,6 +6882,13 @@ const resources = {
|
||||
syncFailed: 'Đồng bộ thất bại: {{error}}',
|
||||
providerAuthSuccess: 'Xác thực {{provider}} thành công',
|
||||
providerDeviceCodeInCallback: 'Nhà cung cấp trả về Device Code flow trong chế độ callback',
|
||||
providerAuthTimeout: 'Đã hết thời gian xác thực. Vui lòng thử lại.',
|
||||
providerAccountRegistrationFailed: 'Không thể đăng ký tài khoản đã xác thực',
|
||||
providerLostStatusEndpoint: 'Mất kết nối với điểm cuối trạng thái xác thực',
|
||||
providerUnknown: 'Nhà cung cấp không xác định: {{provider}}',
|
||||
providerStartOAuthFailed: 'Không thể bắt đầu OAuth',
|
||||
providerCallbackFailed: 'Gửi callback thất bại',
|
||||
providerSubmitCallbackFailed: 'Không thể gửi callback',
|
||||
loggingConfigSaved: 'Đã lưu cấu hình ghi nhật ký.',
|
||||
loggingConfigSaveFailed: 'Không lưu được cấu hình ghi nhật ký.',
|
||||
unifiedConfigUpdated: 'Đã cập nhật cấu hình thành công',
|
||||
@@ -6558,6 +6906,10 @@ const resources = {
|
||||
legacyConfirmFailed: 'Tài khoản cũ "{{name}}" xác nhận thất bại: {{error}}',
|
||||
legacyConfirmSuccess_one: 'Đã xác nhận {{count}} tài khoản cũ',
|
||||
legacyConfirmSuccess_other: 'Đã xác nhận {{count}} tài khoản cũ',
|
||||
legacyConfirmPartial:
|
||||
'Đã xác nhận {{updated}} tài khoản cũ, nhưng {{failed}} lần cập nhật thất bại. Trạng thái tài khoản đã được làm mới.',
|
||||
legacyConfirmAllFailed:
|
||||
'Không thể xác nhận {{failed}} tài khoản cũ. Trạng thái tài khoản đã được làm mới.',
|
||||
noLegacyAccounts: 'Không có tài khoản cũ nào cần xác nhận',
|
||||
routingStrategySet: 'Chiến lược định tuyến đã đặt thành {{strategy}}',
|
||||
variantCreated: 'Đã tạo biến thể thành công',
|
||||
@@ -7921,6 +8273,8 @@ const resources = {
|
||||
},
|
||||
apiProfiles: {
|
||||
title: 'API プロファイル',
|
||||
sidebarTitle: 'API プロファイル',
|
||||
sidebarSubtitle: 'プレミアム API、ローカルランタイム、カスタムエンドポイント',
|
||||
new: '新規',
|
||||
searchPlaceholder: 'プロファイルを検索...',
|
||||
loadingProfiles: 'プロファイルを読み込み中...',
|
||||
@@ -7942,6 +8296,19 @@ const resources = {
|
||||
unsavedChangesDesc:
|
||||
'"{{current}}" に未保存の変更があります。破棄して "{{next}}" に切り替えますか?',
|
||||
discardSwitch: '破棄して切り替え',
|
||||
noOrphansFound: '孤立したプロファイル設定は見つかりませんでした',
|
||||
confirmRegisterOrphans:
|
||||
'{{total}} 件の孤立したプロファイル設定ファイルが見つかりました。うち {{valid}} 件は登録可能です。今すぐ登録しますか?',
|
||||
registeredWithSkipped: '({{count}} 件スキップ)',
|
||||
registeredProfiles: '{{count}} 件のプロファイルを登録しました',
|
||||
copyPrompt: '「{{name}}」をどの新しいプロファイル名にコピーしますか?',
|
||||
destinationEmpty: 'コピー先のプロファイル名は空にできません',
|
||||
exportRedacted:
|
||||
'トークンをマスクした状態でエクスポートしました。必要なら CLI の include-secrets フローを使ってください。',
|
||||
exportDownloaded: 'プロファイルのエクスポートをダウンロードしました',
|
||||
importFailed: 'プロファイルバンドルをインポートできませんでした',
|
||||
discoverOrphans: '孤立プロファイルを検出',
|
||||
importProfileBundle: 'プロファイルバンドルをインポート',
|
||||
},
|
||||
accountsPage: {
|
||||
title: 'アカウント',
|
||||
@@ -8504,6 +8871,93 @@ const resources = {
|
||||
configYaml: 'config.yaml',
|
||||
flow: 'フロー',
|
||||
docsTab: 'ドキュメント',
|
||||
features: '機能',
|
||||
featuresDesc: 'CCS が安全に管理できる Codex の機能フラグを切り替えます。',
|
||||
configOnlyFlags: '設定ファイル内のみの既存フラグ',
|
||||
configOnlyFlagsDesc:
|
||||
'これらのキーはすでに `config.toml` に存在するため、CCS は完全なカタログ対応を主張せずに表示できます。',
|
||||
existing: '既存',
|
||||
nonBooleanForm:
|
||||
'非 boolean 形式で保存されています。編集する場合は Raw TOML を使用してください。',
|
||||
discoveredFromFile: 'CCS の内蔵カタログではなく現在のファイルから検出されました。',
|
||||
featureMultiAgentLabel: 'Multi-agent',
|
||||
featureMultiAgentDesc: 'subagent 連携ツールを有効にします。',
|
||||
featureUnifiedExecLabel: 'Unified exec',
|
||||
featureUnifiedExecDesc: 'PTY ベースの unified exec ツールを使用します。',
|
||||
featureShellSnapshotLabel: 'Shell snapshot',
|
||||
featureShellSnapshotDesc: 'shell 環境スナップショットを再利用します。',
|
||||
featureApplyPatchLabel: 'Apply patch',
|
||||
featureApplyPatchDesc: '自由形式の apply_patch 編集を有効にします。',
|
||||
featureJsReplLabel: 'JS REPL',
|
||||
featureJsReplDesc: 'Node ベースの JavaScript REPL を有効にします。',
|
||||
featureRuntimeMetricsLabel: 'Runtime metrics',
|
||||
featureRuntimeMetricsDesc: 'Codex のランタイム指標を収集します。',
|
||||
featurePreventIdleSleepLabel: 'Prevent idle sleep',
|
||||
featurePreventIdleSleepDesc: '動作中にマシンがスリープしないようにします。',
|
||||
featureFastModeLabel: 'Fast mode',
|
||||
featureFastModeDesc: '高速サービス階層パスを許可します。',
|
||||
featureAppsLabel: 'Apps',
|
||||
featureAppsDesc: 'ChatGPT Apps と connector のサポートを有効にします。',
|
||||
featureSmartApprovalsLabel: 'Smart approvals',
|
||||
featureSmartApprovalsDesc: '対象となる承認を guardian フローに回します。',
|
||||
howCodexWorks: 'CCS での Codex の動作',
|
||||
nativeConfigLabel: 'ネイティブ設定:',
|
||||
nativeConfigDesc: 'ccs-codex と ccsx は保存済みデフォルトでネイティブ Codex を起動します。',
|
||||
transientOverridesLabel: '一時オーバーライド:',
|
||||
transientOverridesDesc:
|
||||
'ccsxp(または ccs codex --target codex)は CCS プロバイダーのショートカットを使います。',
|
||||
cliproxyDefaultLabel: 'CLIProxy デフォルト:',
|
||||
cliproxyDefaultDesc:
|
||||
'通常の codex を CLIProxy 経由にするには、model_provider = "cliproxy" を設定し、下のレシピを追加します。',
|
||||
runtimeInstall: 'ランタイムのインストール',
|
||||
detected: '検出済み',
|
||||
notFound: '見つかりません',
|
||||
detectionSource: '検出元',
|
||||
binaryPath: 'バイナリパス',
|
||||
installDirectory: 'インストール先',
|
||||
versionLabel: 'バージョン',
|
||||
nativeAliases: 'ネイティブ別名',
|
||||
ccsProviderShortcut: 'CCS プロバイダーショートカット',
|
||||
configOverrideSupport: '--config オーバーライド対応',
|
||||
available: '利用可能',
|
||||
missing: '不足',
|
||||
cliproxyNativeCodex: 'CLIProxy 経由のネイティブ Codex',
|
||||
builtInLabel: '内蔵:',
|
||||
builtInCcsxpDesc: 'CCS プロバイダーショートカットとして ccsxp を使用します。',
|
||||
nativeRecipeLabel: 'ネイティブ:',
|
||||
nativeRecipeDesc: '下のレシピを設定すると codex を直接 CLIProxy 経由で利用できます。',
|
||||
saveProviderNamedCliproxy:
|
||||
'上の base URL と env key を使って cliproxy という名前の provider を保存します。',
|
||||
inTopLevelSetDefault: 'Top-level settings で Default provider を cliproxy に設定します。',
|
||||
exportCliproxyApiKey:
|
||||
'ネイティブ Codex を起動する前に shell で CLIPROXY_API_KEY を export してください。',
|
||||
noConfigOverrides:
|
||||
'この Codex ビルドでもネイティブパスは使えますが、ccsxp や ccs codex --target codex などの CCS 側ルーティングは、検出された Codex バイナリが --config オーバーライドに対応するまで利用できません。',
|
||||
configFile: '設定ファイル',
|
||||
path: 'パス',
|
||||
resolved: '解決先',
|
||||
size: 'サイズ',
|
||||
lastModified: '最終更新',
|
||||
tomlWarning: 'TOML 警告',
|
||||
readWarning: '読み込み警告',
|
||||
currentUserLayerSummary: '現在のユーザーレイヤー概要',
|
||||
activeProfile: 'アクティブプロファイル',
|
||||
notSet: '未設定',
|
||||
providersCount: 'providers: {{count}}',
|
||||
profilesCount: 'profiles: {{count}}',
|
||||
enabledFeaturesCount: 'enabled features: {{count}}',
|
||||
mcpServersCount: 'mcp servers: {{count}}',
|
||||
userLayerKeysPresent: '現在存在するユーザーレイヤーキー',
|
||||
nativeShortAlias: 'ネイティブ短縮エイリアス',
|
||||
ccsCodexShortcut: 'CCS Codex ショートカット',
|
||||
explicitProviderRoute: '明示的な provider ルート',
|
||||
runtimeVsProvider: 'ランタイムと provider の違い',
|
||||
honorsSavedNativeConfig: '保存済みのネイティブ設定を尊重',
|
||||
usesTransientOverrides: '一時オーバーライドを使用',
|
||||
unavailableNoConfig: '利用不可(この Codex ビルドには --config サポートがありません)。',
|
||||
yes: 'はい',
|
||||
no: 'いいえ',
|
||||
warningsTitle: '警告',
|
||||
},
|
||||
codexPage: {
|
||||
title: 'Codex',
|
||||
@@ -8913,6 +9367,11 @@ const resources = {
|
||||
fiveHourResets: '5時間リセット {{time}}',
|
||||
weeklyResets: '週間リセット {{time}}',
|
||||
},
|
||||
utils: {
|
||||
codeReview5h: 'コードレビュー(5時間)',
|
||||
codeReviewWeekly: 'コードレビュー(週次)',
|
||||
codeReview: 'コードレビュー',
|
||||
},
|
||||
rawEditorSection: {
|
||||
rawConfig: 'Raw 設定',
|
||||
},
|
||||
@@ -8946,6 +9405,8 @@ const resources = {
|
||||
conflictDesc:
|
||||
'この設定ファイルは別のプロセスで変更されました。変更で上書きしますか?それとも破棄しますか?',
|
||||
overwrite: '上書き',
|
||||
invalidJson: '無効な JSON です',
|
||||
failedSave: '保存に失敗しました',
|
||||
},
|
||||
settingsPage: {
|
||||
title: '設定',
|
||||
@@ -9080,6 +9541,13 @@ const resources = {
|
||||
providerAuthSuccess: '{{provider}} の認証に成功しました',
|
||||
providerDeviceCodeInCallback:
|
||||
'コールバックモードでプロバイダーがデバイスコードフローを返しました',
|
||||
providerAuthTimeout: '認証がタイムアウトしました。もう一度お試しください。',
|
||||
providerAccountRegistrationFailed: '認証済みアカウントを登録できませんでした',
|
||||
providerLostStatusEndpoint: '認証ステータスエンドポイントとの接続が失われました',
|
||||
providerUnknown: '不明なプロバイダーです: {{provider}}',
|
||||
providerStartOAuthFailed: 'OAuth を開始できませんでした',
|
||||
providerCallbackFailed: 'コールバック送信に失敗しました',
|
||||
providerSubmitCallbackFailed: 'コールバックを送信できませんでした',
|
||||
loggingConfigSaved: 'ログ設定を保存しました。',
|
||||
loggingConfigSaveFailed: 'ログ設定の保存に失敗しました。',
|
||||
unifiedConfigUpdated: '設定を更新しました',
|
||||
@@ -9097,6 +9565,10 @@ const resources = {
|
||||
legacyConfirmFailed: 'レガシーアカウント「{{name}}」の確認に失敗しました: {{error}}',
|
||||
legacyConfirmSuccess_one: '{{count}} 件のレガシーアカウントを確認しました',
|
||||
legacyConfirmSuccess_other: '{{count}} 件のレガシーアカウントを確認しました',
|
||||
legacyConfirmPartial:
|
||||
'{{updated}} 件のレガシーアカウントを確認しましたが、{{failed}} 件の更新に失敗しました。アカウント状態を再取得しました。',
|
||||
legacyConfirmAllFailed:
|
||||
'{{failed}} 件のレガシーアカウントの確認に失敗しました。アカウント状態を再取得しました。',
|
||||
noLegacyAccounts: '確認が必要なレガシーアカウントはありません',
|
||||
routingStrategySet: 'ルーティング戦略を {{strategy}} に設定しました',
|
||||
variantCreated: 'バリアントを作成しました',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import i18n from '@/lib/i18n';
|
||||
|
||||
export type SupportStatus = 'new' | 'stable' | 'planned';
|
||||
|
||||
export type SupportScope = 'target' | 'cliproxy' | 'api-profiles' | 'websearch';
|
||||
@@ -47,14 +49,14 @@ export interface CliSupportEntry {
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export const SUPPORT_SCOPE_LABELS: Record<SupportScope, string> = {
|
||||
target: 'Target CLI', // TODO i18n: missing key for support scope target
|
||||
cliproxy: 'CLIProxy Provider', // TODO i18n: missing key for support scope cliproxy
|
||||
'api-profiles': 'API Profile', // TODO i18n: missing key for support scope api-profiles
|
||||
websearch: 'WebSearch', // TODO i18n: missing key for support scope websearch
|
||||
const BASE_SUPPORT_SCOPE_LABELS: Record<SupportScope, string> = {
|
||||
target: 'Target CLI',
|
||||
cliproxy: 'CLIProxy Provider',
|
||||
'api-profiles': 'API Profile',
|
||||
websearch: 'WebSearch',
|
||||
};
|
||||
|
||||
export const SUPPORT_NOTICES: SupportNotice[] = [
|
||||
const BASE_SUPPORT_NOTICES: SupportNotice[] = [
|
||||
{
|
||||
id: 'codex-target-runtime-support',
|
||||
title: 'Native Codex runtime support is live',
|
||||
@@ -204,7 +206,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const CLI_SUPPORT_ENTRIES: CliSupportEntry[] = [
|
||||
const BASE_CLI_SUPPORT_ENTRIES: CliSupportEntry[] = [
|
||||
{
|
||||
id: 'claude-target',
|
||||
name: 'Claude Code',
|
||||
@@ -348,20 +350,86 @@ export const CLI_SUPPORT_ENTRIES: CliSupportEntry[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const SUPPORT_ENTRY_LOOKUP = new Map(CLI_SUPPORT_ENTRIES.map((entry) => [entry.id, entry]));
|
||||
function tx(key: string, defaultValue: string, options?: Record<string, unknown>): string {
|
||||
return i18n.t(key, { defaultValue, ...options });
|
||||
}
|
||||
|
||||
export function getSupportScopeLabels(): Record<SupportScope, string> {
|
||||
return {
|
||||
target: tx('supportCatalog.scope.target', BASE_SUPPORT_SCOPE_LABELS.target),
|
||||
cliproxy: tx('supportCatalog.scope.cliproxy', BASE_SUPPORT_SCOPE_LABELS.cliproxy),
|
||||
'api-profiles': tx(
|
||||
'supportCatalog.scope.apiProfiles',
|
||||
BASE_SUPPORT_SCOPE_LABELS['api-profiles']
|
||||
),
|
||||
websearch: tx('supportCatalog.scope.websearch', BASE_SUPPORT_SCOPE_LABELS.websearch),
|
||||
};
|
||||
}
|
||||
|
||||
function localizeSupportNotice(notice: SupportNotice): SupportNotice {
|
||||
return {
|
||||
...notice,
|
||||
title: tx(`supportCatalog.notice.${notice.id}.title`, notice.title),
|
||||
summary: tx(`supportCatalog.notice.${notice.id}.summary`, notice.summary),
|
||||
primaryAction: tx(`supportCatalog.notice.${notice.id}.primaryAction`, notice.primaryAction),
|
||||
highlights: notice.highlights.map((highlight, index) =>
|
||||
tx(`supportCatalog.notice.${notice.id}.highlight.${index}`, highlight)
|
||||
),
|
||||
actions: notice.actions.map((action) => ({
|
||||
...action,
|
||||
label: tx(`supportCatalog.notice.${notice.id}.action.${action.id}.label`, action.label),
|
||||
description: tx(
|
||||
`supportCatalog.notice.${notice.id}.action.${action.id}.description`,
|
||||
action.description
|
||||
),
|
||||
})),
|
||||
routes: notice.routes.map((route, index) => ({
|
||||
...route,
|
||||
label: tx(`supportCatalog.notice.${notice.id}.route.${index}.label`, route.label),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function localizeCliSupportEntry(entry: CliSupportEntry): CliSupportEntry {
|
||||
return {
|
||||
...entry,
|
||||
name: tx(`supportCatalog.entry.${entry.id}.name`, entry.name),
|
||||
summary: tx(`supportCatalog.entry.${entry.id}.summary`, entry.summary),
|
||||
pillars: {
|
||||
baseUrl: tx(`supportCatalog.entry.${entry.id}.pillar.baseUrl`, entry.pillars.baseUrl),
|
||||
auth: tx(`supportCatalog.entry.${entry.id}.pillar.auth`, entry.pillars.auth),
|
||||
model: tx(`supportCatalog.entry.${entry.id}.pillar.model`, entry.pillars.model),
|
||||
},
|
||||
routes: entry.routes.map((route, index) => ({
|
||||
...route,
|
||||
label: tx(`supportCatalog.entry.${entry.id}.route.${index}.label`, route.label),
|
||||
})),
|
||||
notes: entry.notes ? tx(`supportCatalog.entry.${entry.id}.notes`, entry.notes) : entry.notes,
|
||||
};
|
||||
}
|
||||
|
||||
export function getSupportNotices(): SupportNotice[] {
|
||||
return BASE_SUPPORT_NOTICES.map(localizeSupportNotice);
|
||||
}
|
||||
|
||||
export function getCliSupportEntries(): CliSupportEntry[] {
|
||||
return BASE_CLI_SUPPORT_ENTRIES.map(localizeCliSupportEntry);
|
||||
}
|
||||
|
||||
export function getSupportEntriesForNotice(notice: SupportNotice): CliSupportEntry[] {
|
||||
const supportEntryLookup = new Map(getCliSupportEntries().map((entry) => [entry.id, entry]));
|
||||
return notice.entryIds
|
||||
.map((entryId) => SUPPORT_ENTRY_LOOKUP.get(entryId))
|
||||
.map((entryId) => supportEntryLookup.get(entryId))
|
||||
.filter((entry): entry is CliSupportEntry => Boolean(entry));
|
||||
}
|
||||
|
||||
export function getLatestSupportNotice(): SupportNotice | null {
|
||||
if (SUPPORT_NOTICES.length === 0) {
|
||||
const notices = getSupportNotices();
|
||||
if (notices.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [...SUPPORT_NOTICES].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))[0];
|
||||
return [...notices].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))[0];
|
||||
}
|
||||
|
||||
export function formatCatalogDate(value: string): string {
|
||||
|
||||
@@ -38,10 +38,6 @@ import type { ProviderPreset } from '@/lib/provider-presets';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CopyButton } from '@/components/ui/copy-button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
// TODO i18n: missing keys for apiProfiles: noOrphansFound, confirmRegisterOrphans,
|
||||
// registeredWithSkipped, registeredProfiles, copyPrompt, destinationEmpty,
|
||||
// exportRedacted, exportDownloaded, importFailed, sidebarTitle, sidebarSubtitle,
|
||||
// discoverOrphans, importProfileBundle
|
||||
import { toast } from 'sonner';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
|
||||
@@ -227,7 +227,10 @@ function TargetStatusCard({
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border bg-muted/25 p-3 text-sm text-muted-foreground">
|
||||
{status?.message || 'Verify the binding after saving to inspect the current file state.'}
|
||||
{status?.message ||
|
||||
t('claudeExtensionPage.verifyAfterSaving', {
|
||||
defaultValue: 'Verify the binding after saving to inspect the current file state.',
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
@@ -374,7 +377,15 @@ export function ClaudeExtensionPage() {
|
||||
|
||||
async function handleDelete(): Promise<void> {
|
||||
if (!effectiveSelectedBindingId || !selectedBinding) return;
|
||||
if (!window.confirm(`Delete binding "${selectedBinding.name}"?`)) return;
|
||||
if (
|
||||
!window.confirm(
|
||||
t('claudeExtensionPage.deleteBindingConfirm', {
|
||||
name: selectedBinding.name,
|
||||
defaultValue: 'Delete binding "{{name}}"?',
|
||||
})
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
await deleteBinding.mutateAsync(effectiveSelectedBindingId);
|
||||
const remaining = bindings.filter((binding) => binding.id !== effectiveSelectedBindingId);
|
||||
@@ -419,8 +430,9 @@ export function ClaudeExtensionPage() {
|
||||
<div>
|
||||
<h1 className="font-semibold">{t('claudeExtensionPage.title')}</h1>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{/* TODO i18n: missing key for subtitle */}
|
||||
Saved IDE bindings for CCS profiles
|
||||
{t('claudeExtensionPage.savedBindingsSubtitle', {
|
||||
defaultValue: 'Saved IDE bindings for CCS profiles',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -431,8 +443,7 @@ export function ClaudeExtensionPage() {
|
||||
</div>
|
||||
<Button size="sm" onClick={startCreateMode} className="gap-1.5">
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
{/* TODO i18n: missing key for "New" */}
|
||||
New
|
||||
{t('claudeExtensionPage.new', { defaultValue: 'New' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -442,34 +453,45 @@ export function ClaudeExtensionPage() {
|
||||
<Card className="border-border/60 bg-card/80">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
{/* TODO i18n: missing key for "Create binding"/"Binding editor" */}
|
||||
{creating ? 'Create binding' : 'Binding editor'}
|
||||
{creating
|
||||
? t('claudeExtensionPage.createBinding', { defaultValue: 'Create binding' })
|
||||
: t('claudeExtensionPage.bindingEditor', { defaultValue: 'Binding editor' })}
|
||||
</CardTitle>
|
||||
{/* TODO i18n: missing key for binding editor description */}
|
||||
<CardDescription>
|
||||
Save a profile + IDE path once, then apply or reset it from the dashboard.
|
||||
{t('claudeExtensionPage.bindingEditorDescription', {
|
||||
defaultValue:
|
||||
'Save a profile + IDE path once, then apply or reset it from the dashboard.',
|
||||
})}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
{/* TODO i18n: missing key for "Binding name" */}
|
||||
<div className="text-sm font-medium">Binding name</div>
|
||||
<div className="text-sm font-medium">
|
||||
{t('settingsPage.thinkingSection.bindingName')}
|
||||
</div>
|
||||
<Input
|
||||
value={currentDraft.name}
|
||||
onChange={(event) => updateDraft('name', event.target.value)}
|
||||
placeholder="VS Code · work profile" /* TODO i18n: missing key */
|
||||
placeholder={t('claudeExtensionPage.bindingNamePlaceholder', {
|
||||
defaultValue: 'VS Code · work profile',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{/* TODO i18n: missing key for "CCS profile" */}
|
||||
<div className="text-sm font-medium">CCS profile</div>
|
||||
<div className="text-sm font-medium">
|
||||
{t('claudeExtensionPage.ccsProfile', { defaultValue: 'CCS profile' })}
|
||||
</div>
|
||||
<Select
|
||||
value={currentDraft.profile}
|
||||
onValueChange={(value) => updateDraft('profile', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a profile" /* TODO i18n: missing key */ />
|
||||
<SelectValue
|
||||
placeholder={t('claudeExtensionPage.selectProfile', {
|
||||
defaultValue: 'Select a profile',
|
||||
})}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{profiles.map((profile) => (
|
||||
@@ -481,7 +503,9 @@ export function ClaudeExtensionPage() {
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{selectedProfile?.description ||
|
||||
'Choose which CCS profile the IDE should inherit.'}
|
||||
t('claudeExtensionPage.chooseProfileHint', {
|
||||
defaultValue: 'Choose which CCS profile the IDE should inherit.',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -494,7 +518,11 @@ export function ClaudeExtensionPage() {
|
||||
onValueChange={(value) => updateDraft('host', value as BindingDraft['host'])}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a host" /* TODO i18n: missing key */ />
|
||||
<SelectValue
|
||||
placeholder={t('claudeExtensionPage.selectHost', {
|
||||
defaultValue: 'Select a host',
|
||||
})}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{hosts.map((host) => (
|
||||
@@ -515,22 +543,29 @@ export function ClaudeExtensionPage() {
|
||||
onChange={(event) => updateDraft('ideSettingsPath', event.target.value)}
|
||||
placeholder={
|
||||
selectedHost?.defaultSettingsPath ||
|
||||
'Leave blank for the default user settings path' /* TODO i18n: missing key */
|
||||
t('claudeExtensionPage.ideSettingsPathPlaceholder', {
|
||||
defaultValue: 'Leave blank for the default user settings path',
|
||||
})
|
||||
}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Leave blank to use the default user settings path for{' '}
|
||||
{t('claudeExtensionPage.ideSettingsPathHint', {
|
||||
defaultValue: 'Leave blank to use the default user settings path for',
|
||||
})}{' '}
|
||||
{selectedHost?.label || 'this IDE'}.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{/* TODO i18n: missing key for "Notes" */}
|
||||
<div className="text-sm font-medium">Notes</div>
|
||||
<div className="text-sm font-medium">
|
||||
{t('settingsPage.thinkingSection.notes')}
|
||||
</div>
|
||||
<Input
|
||||
value={currentDraft.notes}
|
||||
onChange={(event) => updateDraft('notes', event.target.value)}
|
||||
placeholder="Optional reminder for this machine or workspace" /* TODO i18n: missing key */
|
||||
placeholder={t('claudeExtensionPage.notesPlaceholder', {
|
||||
defaultValue: 'Optional reminder for this machine or workspace',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -545,12 +580,12 @@ export function ClaudeExtensionPage() {
|
||||
) : (
|
||||
<Save className="h-3.5 w-3.5" />
|
||||
)}
|
||||
{/* TODO i18n: missing key for "Create"/"Save" */}
|
||||
{creating ? 'Create' : 'Save'}
|
||||
{creating
|
||||
? t('claudeExtensionPage.create', { defaultValue: 'Create' })
|
||||
: t('claudeExtensionPage.save', { defaultValue: 'Save' })}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={startCreateMode}>
|
||||
{/* TODO i18n: missing key for "Reset form" */}
|
||||
Reset form
|
||||
{t('claudeExtensionPage.resetForm', { defaultValue: 'Reset form' })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -562,8 +597,7 @@ export function ClaudeExtensionPage() {
|
||||
disabled={deleteBinding.isPending}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
{/* TODO i18n: missing key for "Delete binding" */}
|
||||
Delete binding
|
||||
{t('claudeExtensionPage.deleteBinding', { defaultValue: 'Delete binding' })}
|
||||
</Button>
|
||||
) : null}
|
||||
</CardContent>
|
||||
@@ -571,8 +605,7 @@ export function ClaudeExtensionPage() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="px-1 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{/* TODO i18n: missing key for "Saved bindings" */}
|
||||
Saved bindings
|
||||
{t('claudeExtensionPage.savedBindings', { defaultValue: 'Saved bindings' })}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{bindings.length > 0 ? (
|
||||
@@ -591,9 +624,10 @@ export function ClaudeExtensionPage() {
|
||||
) : (
|
||||
<Card className="border-dashed border-border/60 bg-card/60">
|
||||
<CardContent className="pt-6 text-sm text-muted-foreground">
|
||||
{/* TODO i18n: missing key for empty bindings text */}
|
||||
No saved bindings yet. Create one to manage apply, reset, and drift checks
|
||||
from the dashboard.
|
||||
{t('claudeExtensionPage.emptyBindings', {
|
||||
defaultValue:
|
||||
'No saved bindings yet. Create one to manage apply, reset, and drift checks from the dashboard.',
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
@@ -626,13 +660,16 @@ export function ClaudeExtensionPage() {
|
||||
</div>
|
||||
<div className="max-w-5xl">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
{/* TODO i18n: missing key for default binding name */}
|
||||
{selectedBinding?.name || 'Claude extension binding'}
|
||||
{selectedBinding?.name ||
|
||||
t('claudeExtensionPage.defaultBindingName', {
|
||||
defaultValue: 'Claude extension binding',
|
||||
})}
|
||||
</h2>
|
||||
{/* TODO i18n: missing key for binding description */}
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Manage the shared Claude settings file and the IDE-local settings file as two
|
||||
scoped targets.
|
||||
{t('claudeExtensionPage.bindingDescription', {
|
||||
defaultValue:
|
||||
'Manage the shared Claude settings file and the IDE-local settings file as two scoped targets.',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -648,8 +685,7 @@ export function ClaudeExtensionPage() {
|
||||
) : (
|
||||
<RefreshCw className="mr-1.5 h-3.5 w-3.5" />
|
||||
)}
|
||||
{/* TODO i18n: missing key for "Verify" */}
|
||||
Verify
|
||||
{t('claudeExtensionPage.verify', { defaultValue: 'Verify' })}
|
||||
</Button>
|
||||
{setup ? (
|
||||
<CopyButton value={setup.sharedSettings.command} label="Copy persist command" />
|
||||
@@ -669,8 +705,9 @@ export function ClaudeExtensionPage() {
|
||||
{!activeError ? (
|
||||
<Tabs defaultValue="overview" className="flex flex-col gap-6">
|
||||
<TabsList className="w-full justify-start">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>{' '}
|
||||
{/* TODO i18n: missing key */}
|
||||
<TabsTrigger value="overview">
|
||||
{t('claudeExtensionPage.overview', { defaultValue: 'Overview' })}
|
||||
</TabsTrigger>{' '}
|
||||
<TabsTrigger value="advanced">
|
||||
{t('settingsPage.thinkingSection.advanced')}
|
||||
</TabsTrigger>
|
||||
@@ -679,11 +716,20 @@ export function ClaudeExtensionPage() {
|
||||
<TabsContent value="overview" className="mt-0 space-y-6">
|
||||
<div className="grid gap-6 xl:grid-cols-2">
|
||||
<TargetStatusCard
|
||||
title="Shared Claude settings"
|
||||
description="Writes the managed env block inside ~/.claude/settings.json so CLI and IDE behavior stay aligned."
|
||||
title={t('claudeExtensionPage.sharedClaudeSettings', {
|
||||
defaultValue: 'Shared Claude settings',
|
||||
})}
|
||||
description={t('claudeExtensionPage.sharedClaudeSettingsDescription', {
|
||||
defaultValue:
|
||||
'Writes the managed env block inside ~/.claude/settings.json so CLI and IDE behavior stay aligned.',
|
||||
})}
|
||||
status={status?.sharedSettings}
|
||||
applyLabel="Apply shared" /* TODO i18n: missing key */
|
||||
resetLabel="Reset shared" /* TODO i18n: missing key */
|
||||
applyLabel={t('claudeExtensionPage.applyShared', {
|
||||
defaultValue: 'Apply shared',
|
||||
})}
|
||||
resetLabel={t('claudeExtensionPage.resetShared', {
|
||||
defaultValue: 'Reset shared',
|
||||
})}
|
||||
onApply={() => runBindingAction('shared', 'apply')}
|
||||
onReset={() => runBindingAction('shared', 'reset')}
|
||||
disabled={creating}
|
||||
@@ -693,8 +739,8 @@ export function ClaudeExtensionPage() {
|
||||
title={`${selectedHost?.label || 'IDE'} settings.json`}
|
||||
description="Writes only the Anthropic extension keys so unrelated editor preferences stay untouched."
|
||||
status={status?.ideSettings}
|
||||
applyLabel="Apply IDE" /* TODO i18n: missing key */
|
||||
resetLabel="Reset IDE" /* TODO i18n: missing key */
|
||||
applyLabel={t('claudeExtensionPage.applyIde', { defaultValue: 'Apply IDE' })}
|
||||
resetLabel={t('claudeExtensionPage.resetIde', { defaultValue: 'Reset IDE' })}
|
||||
onApply={() => runBindingAction('ide', 'apply')}
|
||||
onReset={() => runBindingAction('ide', 'reset')}
|
||||
disabled={creating}
|
||||
@@ -708,32 +754,41 @@ export function ClaudeExtensionPage() {
|
||||
<CardTitle className="text-base">
|
||||
{t('settingsPage.thinkingSection.resolvedBinding')}
|
||||
</CardTitle>
|
||||
{/* TODO i18n: missing key for resolved binding description */}
|
||||
<CardDescription>
|
||||
The binding uses the same profile resolution as `ccs persist` and `ccs
|
||||
env`.
|
||||
{t('claudeExtensionPage.resolvedBindingDescription', {
|
||||
defaultValue:
|
||||
'The binding uses the same profile resolution as `ccs persist` and `ccs env`.',
|
||||
})}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<DetailRow
|
||||
label="Profile"
|
||||
label={t('claudeExtensionPage.profile', { defaultValue: 'Profile' })}
|
||||
value={setup?.profile.label || currentDraft.profile || 'Not selected'}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Profile type"
|
||||
label={t('claudeExtensionPage.profileType', {
|
||||
defaultValue: 'Profile type',
|
||||
})}
|
||||
value={setup?.profile.profileType || 'Unknown'}
|
||||
/>
|
||||
<DetailRow label="IDE host" value={selectedHost?.label || 'Not selected'} />
|
||||
<DetailRow
|
||||
label="IDE path mode"
|
||||
label={t('claudeExtensionPage.idePathMode', {
|
||||
defaultValue: 'IDE path mode',
|
||||
})}
|
||||
value={
|
||||
currentDraft.ideSettingsPath.trim()
|
||||
? 'Custom path'
|
||||
: 'Default user path'
|
||||
? t('claudeExtensionPage.customPath', { defaultValue: 'Custom path' })
|
||||
: t('claudeExtensionPage.defaultUserPath', {
|
||||
defaultValue: 'Default user path',
|
||||
})
|
||||
}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Effective IDE path"
|
||||
label={t('claudeExtensionPage.effectiveIdePath', {
|
||||
defaultValue: 'Effective IDE path',
|
||||
})}
|
||||
value={
|
||||
status?.ideSettings.path ||
|
||||
currentDraft.ideSettingsPath.trim() ||
|
||||
@@ -748,12 +803,22 @@ export function ClaudeExtensionPage() {
|
||||
}
|
||||
/>
|
||||
<DetailRow
|
||||
label="Persist command"
|
||||
value={setup?.sharedSettings.command || 'Save a valid binding first'}
|
||||
label={t('claudeExtensionPage.persistCommand', {
|
||||
defaultValue: 'Persist command',
|
||||
})}
|
||||
value={
|
||||
setup?.sharedSettings.command ||
|
||||
t('claudeExtensionPage.saveValidBindingFirst', {
|
||||
defaultValue: 'Save a valid binding first',
|
||||
})
|
||||
}
|
||||
mono
|
||||
/>
|
||||
{currentDraft.notes.trim() ? (
|
||||
<DetailRow label="Notes" value={currentDraft.notes.trim()} />
|
||||
<DetailRow
|
||||
label={t('settingsPage.thinkingSection.notes')}
|
||||
value={currentDraft.notes.trim()}
|
||||
/>
|
||||
) : null}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -763,9 +828,11 @@ export function ClaudeExtensionPage() {
|
||||
<CardTitle className="text-base">
|
||||
{t('settingsPage.thinkingSection.managedPayload')}
|
||||
</CardTitle>
|
||||
{/* TODO i18n: missing key for managed payload description */}
|
||||
<CardDescription>
|
||||
Keep the main view short. The full JSON stays in the Advanced tab.
|
||||
{t('claudeExtensionPage.managedPayloadDescription', {
|
||||
defaultValue:
|
||||
'Keep the main view short. The full JSON stays in the Advanced tab.',
|
||||
})}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -788,17 +855,24 @@ export function ClaudeExtensionPage() {
|
||||
{setup?.env.length ? (
|
||||
<div className="space-y-2">
|
||||
<div className="font-medium">
|
||||
CCS will inject {setup.env.length} environment values.
|
||||
{t('claudeExtensionPage.envInjected', {
|
||||
count: setup.env.length,
|
||||
defaultValue: 'CCS will inject {{count}} environment values.',
|
||||
})}
|
||||
</div>
|
||||
<div className="text-muted-foreground">
|
||||
The IDE-local target receives the extension schema. The shared
|
||||
target receives the same env block through Claude settings.
|
||||
{t('claudeExtensionPage.envInjectedDescription', {
|
||||
defaultValue:
|
||||
'The IDE-local target receives the extension schema. The shared target receives the same env block through Claude settings.',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground">
|
||||
This profile resolves to native Claude defaults, so apply/reset mainly
|
||||
clears existing CCS-managed overrides.
|
||||
{t('claudeExtensionPage.nativeDefaultsDescription', {
|
||||
defaultValue:
|
||||
'This profile resolves to native Claude defaults, so apply/reset mainly clears existing CCS-managed overrides.',
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -814,8 +888,9 @@ export function ClaudeExtensionPage() {
|
||||
applyBinding.variables?.target === 'all' ? (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
) : null}
|
||||
{/* TODO i18n: missing key for "Apply both targets" */}
|
||||
Apply both targets
|
||||
{t('claudeExtensionPage.applyBothTargets', {
|
||||
defaultValue: 'Apply both targets',
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -823,14 +898,17 @@ export function ClaudeExtensionPage() {
|
||||
onClick={() => runBindingAction('all', 'reset')}
|
||||
disabled={resetBinding.isPending}
|
||||
>
|
||||
{/* TODO i18n: missing key for "Reset both targets" */}
|
||||
Reset both targets
|
||||
{t('claudeExtensionPage.resetBothTargets', {
|
||||
defaultValue: 'Reset both targets',
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed bg-muted/15 p-4 text-sm text-muted-foreground">
|
||||
{/* TODO i18n: missing key for "Save this draft..." */}
|
||||
Save this draft to unlock apply, reset, and verify actions.
|
||||
{t('claudeExtensionPage.saveDraftToUnlock', {
|
||||
defaultValue:
|
||||
'Save this draft to unlock apply, reset, and verify actions.',
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -905,13 +983,20 @@ export function ClaudeExtensionPage() {
|
||||
<>
|
||||
<div className="grid gap-6 xl:grid-cols-2">
|
||||
<CodeBlockCard
|
||||
title="Shared Claude settings JSON"
|
||||
description="Managed env block for ~/.claude/settings.json."
|
||||
title={t('claudeExtensionPage.sharedSettingsJson', {
|
||||
defaultValue: 'Shared Claude settings JSON',
|
||||
})}
|
||||
description={t('claudeExtensionPage.sharedSettingsJsonDescription', {
|
||||
defaultValue: 'Managed env block for ~/.claude/settings.json.',
|
||||
})}
|
||||
value={setup.sharedSettings.json}
|
||||
/>
|
||||
<CodeBlockCard
|
||||
title={`${selectedHost?.label || 'IDE'} settings JSON`}
|
||||
description={`Anthropic extension snippet for ${selectedHost?.settingsTargetLabel || 'settings.json'}.`}
|
||||
description={t('claudeExtensionPage.ideSettingsJsonDescription', {
|
||||
label: selectedHost?.settingsTargetLabel || 'settings.json',
|
||||
defaultValue: 'Anthropic extension snippet for {{label}}.',
|
||||
})}
|
||||
value={setup.ideSettings.json}
|
||||
/>
|
||||
</div>
|
||||
@@ -921,16 +1006,22 @@ export function ClaudeExtensionPage() {
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<CardTitle className="text-base">
|
||||
Resolved environment payload
|
||||
{t('claudeExtensionPage.resolvedEnvironmentPayload', {
|
||||
defaultValue: 'Resolved environment payload',
|
||||
})}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Exact environment values that the extension receives after CCS
|
||||
expands this profile.
|
||||
{t('claudeExtensionPage.resolvedEnvironmentPayloadDescription', {
|
||||
defaultValue:
|
||||
'Exact environment values that the extension receives after CCS expands this profile.',
|
||||
})}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<CopyButton
|
||||
value={JSON.stringify(setup.env, null, 2)}
|
||||
label="Copy environment payload"
|
||||
label={t('claudeExtensionPage.copyEnvironmentPayload', {
|
||||
defaultValue: 'Copy environment payload',
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { CodexOverviewTab } from '@/components/compatible-cli/codex-overview-tab
|
||||
import { RawConfigEditorPanel } from '@/components/compatible-cli/raw-json-settings-editor-panel';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import {
|
||||
KNOWN_CODEX_FEATURES,
|
||||
getKnownCodexFeatures,
|
||||
readCodexFeatureState,
|
||||
readCodexMcpServers,
|
||||
readCodexModelProviders,
|
||||
@@ -23,6 +23,7 @@ import { safeParseTomlObject } from '@shared/toml-object';
|
||||
|
||||
export function CodexPage() {
|
||||
const { t } = useTranslation();
|
||||
const featureCatalog = getKnownCodexFeatures();
|
||||
const {
|
||||
diagnostics,
|
||||
diagnosticsLoading,
|
||||
@@ -198,7 +199,7 @@ export function CodexPage() {
|
||||
profileEntries={profileEntries}
|
||||
modelProviderEntries={modelProviderEntries}
|
||||
mcpServerEntries={mcpServerEntries}
|
||||
featureCatalog={KNOWN_CODEX_FEATURES}
|
||||
featureCatalog={featureCatalog}
|
||||
featureState={featureState}
|
||||
disabled={structuredControlsDisabled}
|
||||
disabledReason={controlsDisabledReason}
|
||||
|
||||
@@ -185,15 +185,17 @@ export default function ChannelsSection() {
|
||||
<MessageSquare className="h-5 w-5 text-primary" />
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium">{t('settingsPage.channelsSection.title')}</p>
|
||||
{/* TODO i18n: missing key for channels description paragraphs */}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure official Claude channels here, then run <code>ccs</code> normally on a
|
||||
supported native Claude session.
|
||||
{t('settingsPage.channelsSection.configureDescription', {
|
||||
defaultValue:
|
||||
'Configure official Claude channels here, then run `ccs` normally on a supported native Claude session.',
|
||||
})}
|
||||
</p>
|
||||
{/* TODO i18n: missing key for channels storage description */}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
CCS stores only channel selection in <code>config.yaml</code>. Claude keeps the
|
||||
machine-level channel state under <code>~/.claude/channels/</code>.
|
||||
{t('settingsPage.channelsSection.storageDescription', {
|
||||
defaultValue:
|
||||
'CCS stores only channel selection in `config.yaml`. Claude keeps the machine-level channel state under `~/.claude/channels/`.',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -212,13 +214,23 @@ export default function ChannelsSection() {
|
||||
<p className="text-sm opacity-90">{status.summary.nextStep}</p>
|
||||
</div>
|
||||
<div className="min-w-[220px] rounded-lg border border-current/10 bg-background/60 p-3 text-sm text-foreground">
|
||||
{/* TODO i18n: missing key for "Machine checks" */}
|
||||
<p className="font-medium">Machine checks</p>
|
||||
<p className="font-medium">
|
||||
{t('settingsPage.channelsSection.machineChecks', {
|
||||
defaultValue: 'Machine checks',
|
||||
})}
|
||||
</p>
|
||||
<div className="mt-2 space-y-1 text-muted-foreground">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span>Bun</span>
|
||||
{/* TODO i18n: missing key for "Installed"/"Missing" */}
|
||||
<span>{status.bunInstalled ? 'Installed' : 'Missing'}</span>
|
||||
<span>
|
||||
{status.bunInstalled
|
||||
? t('settingsPage.channelsSection.installed', {
|
||||
defaultValue: 'Installed',
|
||||
})
|
||||
: t('settingsPage.channelsSection.missing', {
|
||||
defaultValue: 'Missing',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span>Claude Code</span>
|
||||
@@ -230,8 +242,12 @@ export default function ChannelsSection() {
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<span>Claude auth</span>
|
||||
{/* TODO i18n: missing key for "Unknown" */}
|
||||
<span>{status.auth.authMethod ?? 'Unknown'}</span>
|
||||
<span>
|
||||
{status.auth.authMethod ||
|
||||
t('settingsPage.channelsSection.unknown', {
|
||||
defaultValue: 'Unknown',
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -248,21 +264,36 @@ export default function ChannelsSection() {
|
||||
|
||||
{status && (
|
||||
<div className="rounded-lg border bg-muted/20 p-4">
|
||||
{/* TODO i18n: missing key for "Fastest path" and step descriptions */}
|
||||
<p className="font-medium">Fastest path</p>
|
||||
<p className="font-medium">
|
||||
{t('settingsPage.channelsSection.fastestPath', {
|
||||
defaultValue: 'Fastest path',
|
||||
})}
|
||||
</p>
|
||||
<div className="mt-3 space-y-2 text-sm text-muted-foreground">
|
||||
<p>1. Turn on the channels you want below.</p>
|
||||
<p>2. Save Telegram or Discord bot tokens here if that channel needs one.</p>
|
||||
<p>
|
||||
3. Run <code>ccs</code> or a native Claude account profile. CCS adds{' '}
|
||||
<code>--channels</code> for you on supported runs.
|
||||
{t('settingsPage.channelsSection.fastestStep1', {
|
||||
defaultValue: '1. Turn on the channels you want below.',
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t('settingsPage.channelsSection.fastestStep2', {
|
||||
defaultValue:
|
||||
'2. Save Telegram or Discord bot tokens here if that channel needs one.',
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{t('settingsPage.channelsSection.fastestStep3', {
|
||||
defaultValue:
|
||||
'3. Run `ccs` or a native Claude account profile. CCS adds `--channels` for you on supported runs.',
|
||||
})}
|
||||
</p>
|
||||
<p>{status.supportMessage}</p>
|
||||
</div>
|
||||
<details className="mt-3 rounded-lg border bg-background p-4">
|
||||
{/* TODO i18n: missing key for "Advanced notes and scope" */}
|
||||
<summary className="cursor-pointer text-sm font-medium">
|
||||
Advanced notes and scope
|
||||
{t('settingsPage.channelsSection.advancedNotes', {
|
||||
defaultValue: 'Advanced notes and scope',
|
||||
})}
|
||||
</summary>
|
||||
<div className="mt-3 space-y-2 text-sm text-muted-foreground">
|
||||
<p>{status.accountStatusCaveat}</p>
|
||||
@@ -276,9 +307,10 @@ export default function ChannelsSection() {
|
||||
<div className="rounded-lg border bg-background p-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
{/* TODO i18n: missing key for "If you run ccs now" */}
|
||||
<p className="font-medium">
|
||||
If you run <code>ccs</code> now
|
||||
{t('settingsPage.channelsSection.ifYouRunNow', {
|
||||
defaultValue: 'If you run `ccs` now',
|
||||
})}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">{status.launchPreview.detail}</p>
|
||||
</div>
|
||||
@@ -288,16 +320,20 @@ export default function ChannelsSection() {
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="rounded-md bg-muted px-3 py-2 font-mono text-sm">
|
||||
{/* TODO i18n: missing key for "You type:" */}
|
||||
<span className="text-muted-foreground">You type:</span>{' '}
|
||||
<span className="text-muted-foreground">
|
||||
{t('settingsPage.thinkingSection.youType')}
|
||||
</span>{' '}
|
||||
{status.launchPreview.command}
|
||||
</div>
|
||||
<div className="rounded-md bg-muted px-3 py-2 font-mono text-sm break-all">
|
||||
{/* TODO i18n: missing key for "CCS adds:" */}
|
||||
<span className="text-muted-foreground">CCS adds:</span>{' '}
|
||||
<span className="text-muted-foreground">
|
||||
{t('settingsPage.thinkingSection.ccsAdds')}
|
||||
</span>{' '}
|
||||
{status.launchPreview.appendedArgs.length > 0
|
||||
? status.launchPreview.appendedArgs.join(' ')
|
||||
: '(nothing yet)'}
|
||||
: t('settingsPage.channelsSection.nothingYet', {
|
||||
defaultValue: '(nothing yet)',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{status.launchPreview.skippedMessages.length > 0 && (
|
||||
@@ -367,10 +403,22 @@ export default function ChannelsSection() {
|
||||
<div className="space-y-3 rounded-lg bg-muted/30 p-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{!channel.tokenConfigured && channel.tokenSource === 'process_env'
|
||||
? `The current CCS process already has ${channel.envKey}. Save it here only if you want persistent Claude channel state.`
|
||||
? t('settingsPage.channelsSection.tokenFromProcessEnv', {
|
||||
envKey: channel.envKey,
|
||||
defaultValue:
|
||||
'The current CCS process already has {{envKey}}. Save it here only if you want persistent Claude channel state.',
|
||||
})
|
||||
: channel.tokenConfigured && channel.processEnvAvailable
|
||||
? `${channel.envKey} is saved in Claude channel state, and the current CCS process env also provides it.`
|
||||
: `Save ${channel.envKey} in Claude's official channel env file. The dashboard never reads the token value back after save.`}
|
||||
? t('settingsPage.channelsSection.tokenSavedAndProcessEnv', {
|
||||
envKey: channel.envKey,
|
||||
defaultValue:
|
||||
'{{envKey}} is saved in Claude channel state, and the current CCS process env also provides it.',
|
||||
})
|
||||
: t('settingsPage.channelsSection.tokenSaveHint', {
|
||||
envKey: channel.envKey,
|
||||
defaultValue:
|
||||
"Save {{envKey}} in Claude's official channel env file. The dashboard never reads the token value back after save.",
|
||||
})}
|
||||
</p>
|
||||
{channel.tokenConfigured && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
@@ -385,10 +433,20 @@ export default function ChannelsSection() {
|
||||
onChange={(event) => updateTokenDraft(channel.id, event.target.value)}
|
||||
placeholder={
|
||||
channel.tokenConfigured
|
||||
? `Configured. Enter a new ${channel.envKey} to replace it.`
|
||||
? t('settingsPage.channelsSection.tokenConfiguredPlaceholder', {
|
||||
envKey: channel.envKey,
|
||||
defaultValue: 'Configured. Enter a new {{envKey}} to replace it.',
|
||||
})
|
||||
: !channel.tokenConfigured && channel.tokenSource === 'process_env'
|
||||
? `Using current CCS process env. Enter a new ${channel.envKey} to save it for Claude.`
|
||||
: `Paste ${channel.envKey}`
|
||||
? t('settingsPage.channelsSection.tokenProcessEnvPlaceholder', {
|
||||
envKey: channel.envKey,
|
||||
defaultValue:
|
||||
'Using current CCS process env. Enter a new {{envKey}} to save it for Claude.',
|
||||
})
|
||||
: t('settingsPage.channelsSection.tokenPastePlaceholder', {
|
||||
envKey: channel.envKey,
|
||||
defaultValue: 'Paste {{envKey}}',
|
||||
})
|
||||
}
|
||||
disabled={saving}
|
||||
/>
|
||||
@@ -403,8 +461,9 @@ export default function ChannelsSection() {
|
||||
disabled={saving || !tokenDraft.trim()}
|
||||
>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{/* TODO i18n: missing key for "Save Token" */}
|
||||
Save Token
|
||||
{t('settingsPage.channelsSection.saveToken', {
|
||||
defaultValue: 'Save Token',
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -412,17 +471,19 @@ export default function ChannelsSection() {
|
||||
disabled={saving || !channel.tokenConfigured}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
{/* TODO i18n: missing key for "Clear Saved Token" */}
|
||||
Clear Saved Token
|
||||
{t('settingsPage.channelsSection.clearSavedToken', {
|
||||
defaultValue: 'Clear Saved Token',
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<details className="rounded-lg border bg-background p-4">
|
||||
{/* TODO i18n: missing key for "Claude-side setup commands" */}
|
||||
<summary className="cursor-pointer text-sm font-medium">
|
||||
Claude-side setup commands
|
||||
{t('settingsPage.channelsSection.claudeSideSetupCommands', {
|
||||
defaultValue: 'Claude-side setup commands',
|
||||
})}
|
||||
</summary>
|
||||
<div className="mt-3 space-y-2">
|
||||
{(channel.manualSetupCommands ?? []).map((command) => (
|
||||
@@ -441,10 +502,11 @@ export default function ChannelsSection() {
|
||||
</div>
|
||||
|
||||
<Alert>
|
||||
{/* TODO i18n: missing key for channels injection disclaimer */}
|
||||
<AlertDescription>
|
||||
CCS injects <code>--channels</code> only for the current Claude session. Telegram,
|
||||
Discord, and iMessage stop receiving messages when that Claude session exits.
|
||||
{t('settingsPage.channelsSection.injectionDisclaimer', {
|
||||
defaultValue:
|
||||
'CCS injects `--channels` only for the current Claude session. Telegram, Discord, and iMessage stop receiving messages when that Claude session exits.',
|
||||
})}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -456,11 +518,11 @@ export default function ChannelsSection() {
|
||||
<Label className="text-sm font-medium">
|
||||
{t('profileEditorSections.skipPermissionPrompts')}
|
||||
</Label>
|
||||
{/* TODO i18n: missing key for skip permission description */}
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Optional advanced behavior. CCS adds <code>--dangerously-skip-permissions</code>{' '}
|
||||
only when at least one selected channel is being auto-enabled and you did not
|
||||
already pass a permission flag yourself.
|
||||
{t('settingsPage.channelsSection.skipPermissionDescription', {
|
||||
defaultValue:
|
||||
'Optional advanced behavior. CCS adds `--dangerously-skip-permissions` only when at least one selected channel is being auto-enabled and you did not already pass a permission flag yourself.',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { api, type ImageAnalysisDashboardData } from '@/lib/api-client';
|
||||
import i18n from '@/lib/i18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useRawConfig } from '../../hooks';
|
||||
|
||||
@@ -116,25 +117,24 @@ function backendStateClass(state: ImageAnalysisDashboardData['backends'][number]
|
||||
}
|
||||
}
|
||||
|
||||
// TODO i18n: missing keys for currentTargetModeLabel values
|
||||
function currentTargetModeLabel(
|
||||
mode: ImageAnalysisDashboardData['profiles'][number]['currentTargetMode']
|
||||
): string {
|
||||
switch (mode) {
|
||||
case 'active':
|
||||
return 'Active';
|
||||
return i18n.t('imageAnalysisStatus.badgeReady');
|
||||
case 'bypassed':
|
||||
return 'Bypassed';
|
||||
return i18n.t('imageAnalysisStatus.badgeBypassed');
|
||||
case 'fallback':
|
||||
return 'Native fallback';
|
||||
return i18n.t('imageAnalysisStatus.nativeFallback');
|
||||
case 'setup':
|
||||
return 'Needs setup';
|
||||
return i18n.t('imageAnalysisStatus.badgeSetup');
|
||||
case 'disabled':
|
||||
return 'Disabled';
|
||||
return i18n.t('imageAnalysisStatus.badgeDisabled');
|
||||
case 'native':
|
||||
return 'Native';
|
||||
return i18n.t('imageAnalysisStatus.badgeNative');
|
||||
case 'unresolved':
|
||||
return 'Native only';
|
||||
return i18n.t('imageAnalysisStatus.nativeImageReading');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,57 +157,84 @@ function currentTargetModeClass(
|
||||
}
|
||||
}
|
||||
|
||||
// TODO i18n: missing keys for backendStateLabel values
|
||||
function backendStateLabel(state: ImageBackend['state']): string {
|
||||
switch (state) {
|
||||
case 'starts_on_launch':
|
||||
return 'Starts on launch';
|
||||
return i18n.t('imageAnalysisSection.backendStartsOnLaunch', {
|
||||
defaultValue: 'Starts on launch',
|
||||
});
|
||||
case 'needs_auth':
|
||||
return 'Needs auth';
|
||||
return i18n.t('imageAnalysisStatus.needsAuth');
|
||||
case 'needs_proxy':
|
||||
return 'Needs proxy';
|
||||
return i18n.t('imageAnalysisStatus.needsProxy');
|
||||
case 'review':
|
||||
return 'Review';
|
||||
return i18n.t('imageAnalysisSection.review', { defaultValue: 'Review' });
|
||||
case 'ready':
|
||||
return 'Ready';
|
||||
return i18n.t('imageAnalysisStatus.badgeReady');
|
||||
}
|
||||
}
|
||||
|
||||
// TODO i18n: missing keys for backendStatusNote values
|
||||
function backendStatusNote(backend: ImageBackend | undefined): string | null {
|
||||
if (!backend) {
|
||||
return 'No model configured.';
|
||||
return i18n.t('imageAnalysisSection.noModelConfigured', {
|
||||
defaultValue: 'No model configured.',
|
||||
});
|
||||
}
|
||||
|
||||
switch (backend.state) {
|
||||
case 'needs_auth':
|
||||
return backend.authReason || 'Authenticate to route here.';
|
||||
return (
|
||||
backend.authReason ||
|
||||
i18n.t('imageAnalysisSection.authenticateToRoute', {
|
||||
defaultValue: 'Authenticate to route here.',
|
||||
})
|
||||
);
|
||||
case 'needs_proxy':
|
||||
return backend.proxyReason || 'Proxy unavailable.';
|
||||
return (
|
||||
backend.proxyReason ||
|
||||
i18n.t('imageAnalysisSection.proxyUnavailable', {
|
||||
defaultValue: 'Proxy unavailable.',
|
||||
})
|
||||
);
|
||||
case 'starts_on_launch':
|
||||
return 'Auth ready. Launches locally on demand.';
|
||||
return i18n.t('imageAnalysisSection.authReadyLaunchesLocally', {
|
||||
defaultValue: 'Auth ready. Launches locally on demand.',
|
||||
});
|
||||
case 'review':
|
||||
return 'Needs manual review.';
|
||||
return i18n.t('imageAnalysisSection.needsManualReview', {
|
||||
defaultValue: 'Needs manual review.',
|
||||
});
|
||||
case 'ready':
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO i18n: missing keys for routeSourceLabel values
|
||||
function routeSourceLabel(source: ImageProfile['resolutionSource']): string {
|
||||
switch (source) {
|
||||
case 'profile-backend':
|
||||
return 'Explicit mapping';
|
||||
return i18n.t('imageAnalysisSection.explicitMapping', {
|
||||
defaultValue: 'Explicit mapping',
|
||||
});
|
||||
case 'fallback-backend':
|
||||
return 'Fallback backend';
|
||||
return i18n.t('imageAnalysisSection.fallbackBackend', {
|
||||
defaultValue: 'Fallback backend',
|
||||
});
|
||||
case 'cliproxy-provider':
|
||||
return 'Provider match';
|
||||
return i18n.t('imageAnalysisSection.providerMatch', {
|
||||
defaultValue: 'Provider match',
|
||||
});
|
||||
case 'cliproxy-bridge':
|
||||
return 'Bridge match';
|
||||
return i18n.t('imageAnalysisSection.bridgeMatch', {
|
||||
defaultValue: 'Bridge match',
|
||||
});
|
||||
case 'native-compatible':
|
||||
return 'Native path';
|
||||
return i18n.t('imageAnalysisSection.nativePath', {
|
||||
defaultValue: 'Native path',
|
||||
});
|
||||
case 'copilot-alias':
|
||||
return 'Copilot alias';
|
||||
return i18n.t('imageAnalysisSection.copilotAlias', {
|
||||
defaultValue: 'Copilot alias',
|
||||
});
|
||||
default:
|
||||
return source.replace(/-/g, ' ');
|
||||
}
|
||||
@@ -970,7 +997,9 @@ export default function ImageAnalysisSection() {
|
||||
<div className="mt-2 flex flex-col gap-2 sm:flex-row">
|
||||
<Input
|
||||
className="h-10 flex-1 border-cyan-500/15 bg-background/88 text-base"
|
||||
placeholder="Set model" /* TODO i18n: missing key */
|
||||
placeholder={t('imageAnalysisSection.setModel', {
|
||||
defaultValue: 'Set model',
|
||||
})}
|
||||
value={currentModel}
|
||||
disabled={saving}
|
||||
onChange={(event) =>
|
||||
@@ -999,8 +1028,7 @@ export default function ImageAnalysisSection() {
|
||||
void commitProviderModel(backendId, '');
|
||||
}}
|
||||
>
|
||||
{/* TODO i18n: missing key for "Clear" */}
|
||||
Clear
|
||||
{t('common.clear', { defaultValue: 'Clear' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -1032,7 +1060,9 @@ export default function ImageAnalysisSection() {
|
||||
getInsetPanelClass('emerald')
|
||||
)}
|
||||
>
|
||||
No profiles prefer native reading yet.
|
||||
{t('imageAnalysisSection.noProfilesPreferNative', {
|
||||
defaultValue: 'No profiles prefer native reading yet.',
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-2 md:grid-cols-2">
|
||||
@@ -1051,15 +1081,28 @@ export default function ImageAnalysisSection() {
|
||||
{profile.name}
|
||||
</div>
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{profile.kind === 'variant' ? 'Variant' : 'Profile'}
|
||||
{profile.kind === 'variant'
|
||||
? t('cliproxyPage.variant')
|
||||
: t('apiProfiles.title')}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{profile.nativeImageCapable ? 'Verified' : 'Review'}
|
||||
{profile.nativeImageCapable
|
||||
? t('imageAnalysisStatus.capabilityVerified')
|
||||
: t('imageAnalysisSection.review', {
|
||||
defaultValue: 'Review',
|
||||
})}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] leading-4 text-muted-foreground">
|
||||
{profile.profileModel || 'Model not detected'} ·{' '}
|
||||
{profile.nativeImageReason || 'Native read preferred.'}
|
||||
{profile.profileModel ||
|
||||
t('imageAnalysisSection.modelNotDetected', {
|
||||
defaultValue: 'Model not detected',
|
||||
})}{' '}
|
||||
·{' '}
|
||||
{profile.nativeImageReason ||
|
||||
t('imageAnalysisSection.nativeReadPreferred', {
|
||||
defaultValue: 'Native read preferred.',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
@@ -1101,8 +1144,9 @@ export default function ImageAnalysisSection() {
|
||||
) : (
|
||||
<ChevronDown className="mr-1 h-4 w-4" />
|
||||
)}
|
||||
{/* TODO i18n: missing key for "Hide"/"Show" */}
|
||||
{showProfileRouting ? 'Hide' : 'Show'}
|
||||
{showProfileRouting
|
||||
? t('common.hide', { defaultValue: 'Hide' })
|
||||
: t('common.show', { defaultValue: 'Show' })}
|
||||
</Button>
|
||||
{showProfileRouting && (
|
||||
<Button
|
||||
@@ -1122,8 +1166,9 @@ export default function ImageAnalysisSection() {
|
||||
disabled={configuredBackendIds.length === 0 || saving}
|
||||
>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
{/* TODO i18n: missing key for "Add mapping" */}
|
||||
Add mapping
|
||||
{t('imageAnalysisSection.addMapping', {
|
||||
defaultValue: 'Add mapping',
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -1156,7 +1201,11 @@ export default function ImageAnalysisSection() {
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 text-[11px] leading-4 text-muted-foreground">
|
||||
<span>Direct override</span>
|
||||
<span>
|
||||
{t('imageAnalysisSection.directOverride', {
|
||||
defaultValue: 'Direct override',
|
||||
})}
|
||||
</span>
|
||||
{!(row.profileName.trim() && row.backendId) && (
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
Draft
|
||||
@@ -1176,8 +1225,7 @@ export default function ImageAnalysisSection() {
|
||||
}}
|
||||
>
|
||||
<Trash2 className="mr-1 h-4 w-4" />
|
||||
{/* TODO i18n: missing key for "Remove" */}
|
||||
Remove
|
||||
{t('common.remove', { defaultValue: 'Remove' })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1186,7 +1234,9 @@ export default function ImageAnalysisSection() {
|
||||
value={row.profileName}
|
||||
list="image-profile-suggestions"
|
||||
disabled={saving}
|
||||
placeholder="Profile or variant name" /* TODO i18n: missing key */
|
||||
placeholder={t('imageAnalysisSection.profileOrVariantName', {
|
||||
defaultValue: 'Profile or variant name',
|
||||
})}
|
||||
className="h-10 border-slate-400/15 bg-background/88 text-base"
|
||||
onChange={(event) => {
|
||||
updateMappingRow(row.id, { profileName: event.target.value });
|
||||
@@ -1281,7 +1331,9 @@ export default function ImageAnalysisSection() {
|
||||
{profile.name}
|
||||
</div>
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{profile.kind === 'variant' ? 'Variant' : 'Profile'}
|
||||
{profile.kind === 'variant'
|
||||
? t('cliproxyPage.variant')
|
||||
: t('apiProfiles.title')}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="font-mono text-[10px] uppercase">
|
||||
{profile.target}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { UpdatesDetailsPanel } from '@/components/updates/updates-details-panel';
|
||||
import { UpdatesInboxItem } from '@/components/updates/updates-inbox-item';
|
||||
import {
|
||||
SUPPORT_NOTICES,
|
||||
getSupportNotices,
|
||||
getSupportEntriesForNotice,
|
||||
type SupportNotice,
|
||||
} from '@/lib/support-updates-catalog';
|
||||
@@ -45,9 +45,8 @@ function noticeMatchesQuery(notice: SupportNotice, queryValue: string): boolean
|
||||
|
||||
export function UpdatesPage() {
|
||||
const { t } = useTranslation();
|
||||
const notices = useMemo(
|
||||
() => [...SUPPORT_NOTICES].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)),
|
||||
[]
|
||||
const notices = [...getSupportNotices()].sort((a, b) =>
|
||||
b.publishedAt.localeCompare(a.publishedAt)
|
||||
);
|
||||
const [viewMode, setViewMode] = useState<NoticeViewMode>('inbox');
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Global test configuration and matchers
|
||||
*/
|
||||
|
||||
import '../../src/lib/i18n';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
import { afterEach, vi } from 'vitest';
|
||||
|
||||
@@ -71,7 +71,8 @@ describe('ModelConfigSection presets', () => {
|
||||
expect(screen.queryByText('Free Tier')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Paid Tier')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Claude Opus 4.6 Thinking' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Gemini Pro' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Gemini Pro High' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Gemini Pro Low' })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('extended-context-toggle')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -101,12 +102,12 @@ describe('ModelConfigSection presets', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Gemini Pro' }));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Gemini Pro High' }));
|
||||
|
||||
expect(onApplyPreset).toHaveBeenCalledWith({
|
||||
ANTHROPIC_MODEL: 'gemini-3.9-pro-preview',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'gemini-3.9-pro-preview',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'gemini-3.9-pro-preview',
|
||||
ANTHROPIC_MODEL: 'gemini-3.1-pro-high',
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: 'gemini-3.1-pro-high',
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: 'gemini-3.1-pro-high',
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gemini-3-9-flash-preview',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,13 +73,8 @@ describe('CodexOverviewTab', () => {
|
||||
expect(screen.getAllByText('codex').length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('codex --profile default')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getAllByText(
|
||||
(_, node) =>
|
||||
node?.textContent?.includes(
|
||||
'Built-in openai and oss providers are also valid native defaults'
|
||||
) ?? false
|
||||
).length
|
||||
).toBeGreaterThan(0);
|
||||
screen.getByText('API profiles continue to default to Claude or Droid.')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses the active named profile when one is selected', () => {
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('ProfileCreateDialog', () => {
|
||||
|
||||
expect(screen.getByText('Featured Providers')).toBeInTheDocument();
|
||||
expect(screen.getByText('More Presets')).toBeInTheDocument();
|
||||
expect(screen.getByText('Local Runtimes')).toBeInTheDocument();
|
||||
expect(screen.getByText('Local runtimes')).toBeInTheDocument();
|
||||
expect(screen.getByText('Alibaba Coding Plan')).toBeVisible();
|
||||
expect(screen.getByText('Hugging Face')).toBeVisible();
|
||||
expect(document.body.querySelectorAll('.overflow-x-auto')).toHaveLength(2);
|
||||
@@ -73,7 +73,7 @@ describe('ProfileCreateDialog', () => {
|
||||
|
||||
expect(screen.getByDisplayValue('ollama')).toBeInTheDocument();
|
||||
expect(screen.getByDisplayValue('http://localhost:11434')).toBeInTheDocument();
|
||||
expect(screen.getByText('Local Runtimes')).toBeInTheDocument();
|
||||
expect(screen.getByText('Local runtimes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('steers the Hugging Face preset to the droid target by default', async () => {
|
||||
|
||||
@@ -32,12 +32,23 @@ vi.mock(import('react-i18next'), async (importOriginal) => {
|
||||
'authMonitor.success': 'Success',
|
||||
'authMonitor.failed': 'Failed',
|
||||
'authMonitor.successRate': 'Success Rate',
|
||||
'authMonitorLive.live': 'LIVE',
|
||||
'authMonitorLive.accountMonitor': 'Account Monitor',
|
||||
'authMonitorLive.requestsLabel': 'req',
|
||||
};
|
||||
|
||||
if (key === 'authMonitor.accountsCount') {
|
||||
return `${options?.count ?? 0} accounts`;
|
||||
}
|
||||
|
||||
if (key === 'authMonitorLive.updated') {
|
||||
return `Updated ${options && 'time' in options ? String((options as { time?: string }).time ?? '') : ''}`.trim();
|
||||
}
|
||||
|
||||
if (key === 'authMonitorLive.updatedNow') {
|
||||
return 'Updated now';
|
||||
}
|
||||
|
||||
return translations[key] ?? key;
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CLI_SUPPORT_ENTRIES, SUPPORT_NOTICES } from '@/lib/support-updates-catalog';
|
||||
import { getCliSupportEntries, getSupportNotices } from '@/lib/support-updates-catalog';
|
||||
|
||||
describe('support-updates catalog codex routing', () => {
|
||||
it('routes the Codex runtime notice to the Codex dashboard', () => {
|
||||
const notice = SUPPORT_NOTICES.find((entry) => entry.id === 'codex-target-runtime-support');
|
||||
const notice = getSupportNotices().find((entry) => entry.id === 'codex-target-runtime-support');
|
||||
|
||||
expect(notice).toBeDefined();
|
||||
expect(notice?.routes).toContainEqual({ label: 'Codex CLI', path: '/codex' });
|
||||
@@ -17,14 +17,14 @@ describe('support-updates catalog codex routing', () => {
|
||||
});
|
||||
|
||||
it('routes the Codex target entry to the Codex dashboard', () => {
|
||||
const entry = CLI_SUPPORT_ENTRIES.find((item) => item.id === 'codex-target');
|
||||
const entry = getCliSupportEntries().find((item) => item.id === 'codex-target');
|
||||
|
||||
expect(entry).toBeDefined();
|
||||
expect(entry?.routes).toEqual([{ label: 'Codex CLI', path: '/codex' }]);
|
||||
});
|
||||
|
||||
it('uses Updates Center naming consistently for the rollout notice', () => {
|
||||
const notice = SUPPORT_NOTICES.find((entry) => entry.id === 'updates-center-launch');
|
||||
const notice = getSupportNotices().find((entry) => entry.id === 'updates-center-launch');
|
||||
|
||||
expect(notice).toBeDefined();
|
||||
expect(notice?.title).toContain('Updates Center');
|
||||
|
||||
@@ -166,7 +166,7 @@ describe('ImageAnalysisSection', () => {
|
||||
it('renders global controls and saves updated config', async () => {
|
||||
render(<ImageAnalysisSection />, { withSettingsProvider: true });
|
||||
|
||||
expect(await screen.findByText('Image')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Image Analysis')).toBeInTheDocument();
|
||||
expect(screen.getByText('Partially ready')).toBeInTheDocument();
|
||||
expect(screen.getByText('Core setup')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Native reading').length).toBeGreaterThan(0);
|
||||
@@ -206,7 +206,7 @@ describe('ImageAnalysisSection', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(await screen.findByText('Image settings saved.')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Settings saved')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('allows saving a disabled configuration even when every provider model is cleared', async () => {
|
||||
@@ -278,7 +278,7 @@ describe('ImageAnalysisSection', () => {
|
||||
it('auto-saves edits without rendering a dedicated save button', async () => {
|
||||
const { container } = render(<ImageAnalysisSection />, { withSettingsProvider: true });
|
||||
|
||||
expect(await screen.findByText('Image')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Image Analysis')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Save changes' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Refresh' })).toBeInTheDocument();
|
||||
expect(container.firstElementChild).toHaveClass(
|
||||
|
||||
@@ -143,7 +143,7 @@ describe('LogsPage', () => {
|
||||
|
||||
render(<LogsPage />);
|
||||
|
||||
expect(screen.getByLabelText('Loading logs workspace')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Loading logs...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filters by source and search query', async () => {
|
||||
|
||||
Reference in New Issue
Block a user