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