fix(codex): align cliproxy guidance with runtime behavior

This commit is contained in:
Tam Nhu Tran
2026-03-29 21:04:59 -04:00
parent 0ffde4f3a2
commit bd1ff02521
22 changed files with 342 additions and 64 deletions
+8 -3
View File
@@ -198,16 +198,21 @@ Resolves which adapter to use via `resolveTargetType()`:
```
1. --target <name> flag (highest priority)
2. argv[0] detection (runtime alias pattern):
2. explicit runtime entrypoint (`CCS_INTERNAL_ENTRY_TARGET`):
- ccs-droid / ccsd → droid
- ccs-codex / ccsx → codex
- ccsxp → codex (provider shortcut)
3. argv[0] detection (runtime alias pattern / custom alias map):
- ccs-droid → droid
- ccsd → droid
- ccs-codex → codex
- ccsx → codex
- ccs → default
3. Profile config: profileConfig.target field
4. Profile config: profileConfig.target field
4. Fallback: 'claude' (lowest priority)
5. Fallback: 'claude' (lowest priority)
```
### Registration Pattern
+9 -5
View File
@@ -36,8 +36,9 @@ The main CLI is organized into domain-specific modules with barrel exports.
src/
├── ccs.ts # Main entry point & profile execution flow
├── bin/ # Dedicated runtime entrypoints
│ ├── droid-runtime.ts # argv[0] shim for ccs-droid / ccsd
── codex-runtime.ts # argv[0] shim for ccs-codex / ccsx
│ ├── droid-runtime.ts # Forces droid target for ccs-droid / ccsd package bins
── codex-runtime.ts # Forces codex target for ccs-codex / ccsx package bins
│ └── ccsxp-runtime.ts # Forces built-in codex profile + codex target for ccsxp
├── types/ # TypeScript type definitions
│ ├── index.ts # Barrel export (aggregates all types)
│ ├── cli.ts # CLI types (ParsedArgs, ExitCode)
@@ -69,7 +70,7 @@ src/
│ ├── index.ts # Barrel export
│ ├── target-adapter.ts # TargetAdapter interface contract
│ ├── target-registry.ts # Registry for runtime adapter lookup
│ ├── target-resolver.ts # Resolution logic (flag > argv[0] > config)
│ ├── target-resolver.ts # Resolution logic (flag > runtime entrypoint / argv[0] > config)
│ ├── target-metadata.ts # Runtime vs persisted target metadata and alias lists
│ ├── target-runtime-compatibility.ts # Guardrails for target/profile combinations
│ ├── claude-adapter.ts # Claude Code CLI implementation
@@ -248,7 +249,9 @@ src/
### Native Codex Runtime Target
- Runtime aliases: `ccs-codex` and `ccsx` resolve through `src/bin/codex-runtime.ts` and `src/targets/target-resolver.ts`.
- Dedicated runtime entrypoints: `ccs-codex` and `ccsx` resolve through `src/bin/codex-runtime.ts`, while `ccsxp` resolves through `src/bin/ccsxp-runtime.ts`; all three set `CCS_INTERNAL_ENTRY_TARGET=codex` before delegating to `src/targets/target-resolver.ts`.
- Provider shortcut behavior: `ccsxp` also strips user-supplied `--target` overrides and rewrites argv to `ccs codex --target codex ...`, so it always lands on the built-in Codex-via-CLIProxy route.
- `argv[0]` alias mapping still exists in `src/targets/target-resolver.ts` for same-binary/custom alias scenarios, but the built-in npm bins above do not depend on that map at runtime.
- Metadata boundary: `src/targets/target-metadata.ts` keeps Codex runtime-only in v1, so persisted default targets remain `claude | droid`.
- Compatibility guardrails: `src/targets/target-runtime-compatibility.ts` centralizes which profile types can execute on Codex.
- Adapter behavior: `src/targets/codex-adapter.ts` and `src/targets/codex-detector.ts` launch native Codex without rewriting `~/.codex/config.toml`; CCS-backed routes use transient `codex -c key=value` overrides and env-key injection.
@@ -279,7 +282,8 @@ The targets module provides an extensible interface for dispatching profiles to
2. **Target Resolution** - Priority order:
- `--target <cli>` flag (CLI argument)
- `argv[0]` detection (runtime alias pattern: `ccs-droid` / `ccsd` → droid)
- Explicit runtime entrypoint via `CCS_INTERNAL_ENTRY_TARGET` (used by `src/bin/droid-runtime.ts`, `src/bin/codex-runtime.ts`, and `src/bin/ccsxp-runtime.ts`)
- `argv[0]` detection for custom/same-binary runtime aliases
- Per-profile `target` field (from config.yaml)
- Default: `claude`
+3 -2
View File
@@ -58,7 +58,7 @@ CCS v7.45 introduces the Target Adapter pattern, enabling seamless integration w
Profile Resolution (CLIProxy, Settings/API, Account-based)
|
v
Target Resolution (--target flag > argv[0] > config > default)
Target Resolution (--target flag > runtime entrypoint / argv[0] > config > default)
|
v
Get Target Adapter (Claude, Droid, or Codex)
@@ -91,7 +91,7 @@ Spawn Target Process
- Preserves native `~/.codex/config.toml` ownership
- Dashboard page reads/writes only the user config layer with explicit runtime-vs-provider warnings
**Runtime alias pattern (built-in bins / argv[0]-style):**
**Runtime entrypoints (built-in bins) and argv[0]-style aliases:**
```
ccs → Target: claude (default)
@@ -99,6 +99,7 @@ ccs-droid → Target: droid (explicit alias)
ccsd → Target: droid (legacy shortcut)
ccs-codex → Target: codex (explicit alias)
ccsx → Target: codex (short alias)
ccsxp → Target: codex (provider shortcut; rewrites argv to `ccs codex --target codex`)
```
For details on the adapter architecture, see [Target Adapters](./target-adapters.md).
+33 -14
View File
@@ -85,20 +85,25 @@ CCS resolves which adapter to use via priority-ordered checks:
└─ ccs --target droid glm
└─ ccs --target codex
2. argv[0] detection (runtime alias pattern) — binary name mapping
2. Explicit runtime entrypoint (`CCS_INTERNAL_ENTRY_TARGET`) — dedicated bin shims
└─ ccs-droid / ccsd → droid
└─ ccs-codex / ccsx → codex
└─ ccsxp → codex, then rewrites argv to `ccs codex --target codex ...`
3. argv[0] detection (runtime alias pattern) — binary name mapping for same-binary/custom aliases
└─ ccs-droid (explicit alias) → droid
└─ ccsd (legacy shortcut) → droid
└─ ccs-codex (explicit alias) → codex
└─ ccsx (short alias) → codex
└─ ccs (regular command) → default
3. Per-profile config (from ~/.ccs/config.yaml or settings.json)
4. Per-profile config (from ~/.ccs/config.yaml or settings.json)
└─ persisted targets are currently only `claude` and `droid`
└─ profiles:
glm:
target: droid
4. Fallback: 'claude' — lowest priority
5. Fallback: 'claude' — lowest priority
```
### Implementation
@@ -117,19 +122,25 @@ export function resolveTargetType(
return parsed.targetOverride;
}
// 2. Check argv[0] (binary name)
// 2. Check explicit runtime entrypoint shim
const entrypointTarget = resolveEntrypointTarget();
if (entrypointTarget) {
return entrypointTarget;
}
// 3. Check argv[0] (binary name / custom alias map)
const binName = path.basename(process.argv[1] || process.argv0 || '').replace(/\.(cmd|bat|ps1|exe)$/i, '');
if (ARGV0_TARGET_MAP[binName]) {
return ARGV0_TARGET_MAP[binName];
}
// 3. Check profile config
// 4. Check profile config
if (profileConfig?.target) {
// Persisted targets intentionally exclude runtime-only codex.
return profileConfig.target;
}
// 4. Default to claude
// 5. Default to claude
return 'claude';
}
```
@@ -488,20 +499,27 @@ longer read-mostly:
This keeps the dashboard honest about Codex's merged configuration model while still giving users
one place to inspect and manage the user-owned layer safely.
### Runtime Alias Pattern
### Runtime Entrypoints and argv[0] Fallback
```bash
# Built-in package bin aliases
# Built-in package bin entrypoints
ccs-codex
Target: codex (forced by runtime alias)
dist/bin/codex-runtime.js
CCS_INTERNAL_ENTRY_TARGET=codex
ccsx codex
Target: codex (forced by runtime alias)
→ codex ...args
ccsx
dist/bin/codex-runtime.js
CCS_INTERNAL_ENTRY_TARGET=codex
ccsxp
→ dist/bin/ccsxp-runtime.js
CCS_INTERNAL_ENTRY_TARGET=codex
→ injects built-in codex profile shortcut
```
Runtime aliases can also be extended with `CCS_TARGET_ALIASES` or legacy
`CCS_CODEX_ALIASES` after creating a matching launcher:
If a user launches CCS through a custom shim instead of the built-in package bins, target
resolution falls back to `argv[0]` aliases from `CCS_TARGET_ALIASES` or legacy
`CCS_CODEX_ALIASES`:
```bash
ln -s /path/to/ccs /path/to/mycodex
@@ -757,6 +775,7 @@ ccs --target droid help
# Test Codex adapter (if installed)
ccs --target codex
ccs-codex
ccsxp
# Test argv[0] detection
ccs-droid help
+11 -1
View File
@@ -1,9 +1,19 @@
const { stripTargetFlag } = require('../targets/target-resolver');
const { fail } = require('../utils/ui');
process.env.CCS_INTERNAL_ENTRY_TARGET = 'codex';
// ccsxp is an opinionated shortcut for the built-in Codex-on-Codex route.
// Strip user-supplied target overrides before forcing the shortcut target.
const forwardedArgs = stripTargetFlag(process.argv.slice(2));
const forwardedArgs = (() => {
try {
return stripTargetFlag(process.argv.slice(2));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(fail(message));
process.exit(1);
}
})();
process.argv.splice(2, process.argv.length - 2, 'codex', '--target', 'codex', ...forwardedArgs);
require('../ccs');
+1 -3
View File
@@ -177,9 +177,7 @@ function showHelp(): void {
console.log(
` $ ${color('ccs config channels --clear-token discord', 'command')} ${dim('# Clear one token')}`
);
console.log(
` ${dim('Official Channels only work on native Claude default/account sessions, not on ccs glm or other API/OAuth/Droid targets.')}`
);
console.log(` ${dim(getOfficialChannelsSupportMessage())}`);
console.log('');
}
+2 -2
View File
@@ -1,4 +1,5 @@
import { extractOption, hasAnyFlag, scanCommandArgs } from './arg-extractor';
import { getOfficialChannelsSupportMessage } from '../channels/official-channels-runtime';
const CONFIG_COMMAND_FLAGS = ['--help', '-h', '--port', '-p', '--host', '-H', '--dev'] as const;
@@ -92,8 +93,7 @@ export function showConfigCommandHelp(): void {
console.log(' --set-token <s> Save channel token (telegram=<t> or discord=<t>)');
console.log(' --clear-token Remove all saved channel tokens');
console.log(' --clear-token <c> Remove one saved channel token');
console.log(' Works only for native Claude default/account sessions');
console.log(' Not for ccs glm, other API/OAuth profiles, or Droid targets');
console.log(` ${getOfficialChannelsSupportMessage()}`);
console.log('');
console.log(' auth Manage dashboard authentication');
console.log(' auth setup Configure username and password');
+2 -2
View File
@@ -4,6 +4,7 @@ import { initUI, box, color, dim, sectionHeader, subheader } from '../utils/ui';
import { isUnifiedMode } from '../config/unified-config-loader';
import { getCcsDir, getCcsDirSource } from '../utils/config-manager';
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
import { getOfficialChannelsSupportMessage } from '../channels/official-channels-runtime';
type HelpWriter = (line: string) => void;
@@ -608,8 +609,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
['ccs config channels --clear-token [channel]', 'Remove one or all saved channel tokens'],
['', ''],
['', 'Fastest path: turn on the channel, save the token if needed, then run ccs.'],
['Note:', 'Runtime-only. Applies to native Claude default/account sessions only.'],
['', 'Not supported for ccs glm, other API/OAuth profiles, or Droid targets.'],
['Note:', getOfficialChannelsSupportMessage()],
['', 'Telegram/Discord tokens live in ~/.claude/channels/<channel>/.env.'],
['', 'Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work for that launch.'],
['', 'iMessage is macOS-only and requires local OS permissions instead of a bot token.'],
@@ -72,6 +72,7 @@ const SANDBOX_MODE_VALUES = new Set(['read-only', 'workspace-write', 'danger-ful
const WEB_SEARCH_VALUES = new Set(['cached', 'live', 'disabled']);
const PERSONALITY_VALUES = new Set(['none', 'friendly', 'pragmatic']);
const PROJECT_TRUST_LEVEL_VALUES = new Set(['trusted', 'untrusted']);
const BUILT_IN_CODEX_MODEL_PROVIDERS = new Set(['openai', 'oss']);
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -572,6 +573,10 @@ export function summarizeCodexModelProviders(value: unknown): CodexModelProvider
.sort((left, right) => left.name.localeCompare(right.name));
}
function isBuiltInCodexModelProvider(name: string | null): boolean {
return name !== null && BUILT_IN_CODEX_MODEL_PROVIDERS.has(name);
}
export function summarizeCodexFeatureFlags(value: unknown): {
all: CodexFeatureFlagDiagnostics[];
enabled: CodexFeatureFlagDiagnostics[];
@@ -729,15 +734,15 @@ export async function getCodexDashboardDiagnostics(): Promise<CodexDashboardDiag
}
if (activeModelProvider) {
const activeProvider = modelProviders.find((provider) => provider.name === activeModelProvider);
if (!activeProvider) {
if (!activeProvider && !isBuiltInCodexModelProvider(activeModelProvider)) {
warnings.push(
`Active model provider "${activeModelProvider}" is selected but missing from [model_providers].`
);
} else {
} else if (activeProvider) {
if (!activeProvider.baseUrl) {
warnings.push(`Active model provider "${activeProvider.name}" is missing base_url.`);
}
if (!activeProvider.envKey) {
if (!activeProvider.envKey && !activeProvider.requiresOpenaiAuth) {
warnings.push(`Active model provider "${activeProvider.name}" is missing env_key.`);
}
}
@@ -81,7 +81,8 @@ describe('help command parity', () => {
'Fastest path: turn on the channel, save the token if needed, then run ccs.'
)
).toBe(true);
expect(rendered.includes('Not supported for ccs glm')).toBe(true);
expect(rendered.includes('ccs codex')).toBe(true);
expect(rendered.includes('ccs --target codex')).toBe(true);
expect(
rendered.includes('Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work')
).toBe(true);
@@ -278,6 +278,22 @@ process.exit(0);
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['--version']]);
});
it('fails with a clean CLI error when ccsxp receives a malformed --target flag', () => {
if (process.platform === 'win32') return;
const result = runCcsxpAlias(['--target'], {
...process.env,
CI: '1',
NO_COLOR: '1',
CCS_HOME: tmpHome,
CCS_CODEX_PATH: fakeCodexPath,
});
expect(result.status).toBe(1);
expect(result.stderr).toContain('[X] --target requires a value (claude, droid, codex)');
expect(result.stderr).not.toContain('at parseTargetFlags');
});
it('passes ccs codex --target codex --version through to the native Codex binary', () => {
if (process.platform === 'win32') return;
@@ -234,6 +234,32 @@ wire_api = "responses"
expect(diagnostics.warnings.some((warning) => warning.includes('missing env_key'))).toBe(true);
});
it('does not warn for built-in openai provider without a custom model_providers entry', async () => {
fs.writeFileSync(path.join(codexHome, 'config.toml'), 'model_provider = "openai"\n');
const diagnostics = await getCodexDashboardDiagnostics();
expect(diagnostics.warnings.some((warning) => warning.includes('missing from [model_providers]'))).toBe(
false
);
});
it('allows custom providers that use requires_openai_auth without env_key warnings', async () => {
fs.writeFileSync(
path.join(codexHome, 'config.toml'),
`model_provider = "corp-openai"
[model_providers.corp-openai]
base_url = "https://example.test/v1"
requires_openai_auth = true
`
);
const diagnostics = await getCodexDashboardDiagnostics();
expect(diagnostics.warnings.some((warning) => warning.includes('missing env_key'))).toBe(false);
});
it('summarizes granular approval policies without flattening them to null', async () => {
fs.writeFileSync(
path.join(codexHome, 'config.toml'),
@@ -297,6 +297,7 @@ export function ProviderEditor({
defaultTarget={defaultTarget}
data={data}
authStatus={authStatus}
supportsModelConfig={Boolean(catalog)}
/>
</TabsContent>
</div>
@@ -17,6 +17,7 @@ interface ProviderInfoTabProps {
defaultTarget?: CliTarget;
data?: SettingsResponse;
authStatus: AuthStatus;
supportsModelConfig?: boolean;
}
export function ProviderInfoTab({
@@ -25,10 +26,16 @@ export function ProviderInfoTab({
defaultTarget,
data,
authStatus,
supportsModelConfig = false,
}: ProviderInfoTabProps) {
const resolvedTarget = defaultTarget || 'claude';
const isDroidTarget = resolvedTarget === 'droid';
const isCodexProvider = provider === 'codex';
const managementPrefix =
resolvedTarget === 'claude' ? `ccs ${provider}` : `ccs ${provider} --target claude`;
const changeModelCommand = `${managementPrefix} --config`;
const addAccountCommand = `${managementPrefix} --auth --add`;
const listAccountsCommand = `${managementPrefix} --accounts`;
return (
<ScrollArea className="h-full">
@@ -109,9 +116,30 @@ export function ProviderInfoTab({
label={isDroidTarget ? 'Override to Claude' : 'Run on Droid (--target)'}
command={`ccs ${provider} --target ${isDroidTarget ? 'claude' : 'droid'} "your prompt"`}
/>
<UsageCommand label="Change model" command={`ccs ${provider} --config`} />
<UsageCommand label="Add account" command={`ccs ${provider} --add`} />
<UsageCommand label="List accounts" command={`ccs ${provider} --accounts`} />
{supportsModelConfig && (
<UsageCommand
label={
resolvedTarget === 'claude' ? 'Change model' : 'Change model (Claude target)'
}
command={changeModelCommand}
/>
)}
<UsageCommand
label={resolvedTarget === 'claude' ? 'Add account' : 'Add account (Claude target)'}
command={addAccountCommand}
/>
<UsageCommand
label={
resolvedTarget === 'claude' ? 'List accounts' : 'List accounts (Claude target)'
}
command={listAccountsCommand}
/>
{resolvedTarget !== 'claude' && (
<p className="text-xs text-muted-foreground">
Account and model-management flags stay on Claude target. Codex and Droid runtime
launches reject those CLIProxy management commands.
</p>
)}
</div>
</div>
</div>
@@ -7,6 +7,7 @@ import type {
CompatibleCliProviderDocLink,
CodexDashboardDiagnostics,
} from '@/hooks/use-codex-types';
import { CLIPROXY_NATIVE_CODEX_RECIPE } from '@/lib/codex-config';
const DEFAULT_CODEX_DOC_LINKS = [
{
@@ -44,13 +45,6 @@ const DEFAULT_PROVIDER_DOCS: CompatibleCliProviderDocLink[] = [
},
];
const CLIPROXY_NATIVE_CODEX_RECIPE = `model_provider = "cliproxy"
[model_providers.cliproxy]
base_url = "http://127.0.0.1:8317/api/provider/codex"
env_key = "CLIPROXY_API_KEY"
wire_api = "responses"`;
function renderTextWithLinks(text: string): ReactNode[] {
const urlPattern = /https?:\/\/[^\s)]+/g;
const nodes: ReactNode[] = [];
@@ -22,15 +22,9 @@ import {
TableRow,
} from '@/components/ui/table';
import type { CodexDashboardDiagnostics } from '@/hooks/use-codex-types';
import { CLIPROXY_NATIVE_CODEX_RECIPE } from '@/lib/codex-config';
import { cn } from '@/lib/utils';
const CLIPROXY_NATIVE_CODEX_RECIPE = `model_provider = "cliproxy"
[model_providers.cliproxy]
base_url = "http://127.0.0.1:8317/api/provider/codex"
env_key = "CLIPROXY_API_KEY"
wire_api = "responses"`;
function formatTimestamp(value: number | null | undefined): string {
if (!value || !Number.isFinite(value)) return 'N/A';
return new Date(value).toLocaleString();
@@ -65,6 +59,10 @@ interface CodexOverviewTabProps {
}
export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
const inspectProfileCommand = diagnostics.config.activeProfile
? `codex --profile ${diagnostics.config.activeProfile}`
: 'codex';
return (
<ScrollArea className="h-full">
<div className="space-y-4 pr-1">
@@ -88,6 +86,10 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
<code>[model_providers.cliproxy]</code> entry if you want CLIProxy as the saved native
default.
</p>
<p>
Built-in <code>openai</code> and <code>oss</code> providers are also valid native
defaults and do not need a custom <code>[model_providers]</code> stanza.
</p>
<p>
Saved default targets for API profiles and variants still remain on Claude or Droid.
</p>
@@ -281,9 +283,13 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
description: 'Use the explicit built-in Codex provider route.',
},
{
label: 'Workspace trust',
command: `codex --profile ${diagnostics.config.activeProfile || 'default'}`,
description: 'Inspect the active profile directly in native Codex.',
label: diagnostics.config.activeProfile
? 'Inspect active profile'
: 'Open native Codex',
command: inspectProfileCommand,
description: diagnostics.config.activeProfile
? 'Inspect the active named profile directly in native Codex.'
: 'Open native Codex without forcing a named profile overlay.',
},
]}
/>
+7
View File
@@ -49,6 +49,13 @@ export interface CodexFeatureCatalogEntry {
description: string;
}
export const CLIPROXY_NATIVE_CODEX_RECIPE = `model_provider = "cliproxy"
[model_providers.cliproxy]
base_url = "http://127.0.0.1:8317/api/provider/codex"
env_key = "CLIPROXY_API_KEY"
wire_api = "responses"`;
export const KNOWN_CODEX_FEATURES: CodexFeatureCatalogEntry[] = [
{
name: 'multi_agent',
+4 -4
View File
@@ -950,7 +950,7 @@ const resources = {
actionRequired: 'Action Required',
done: 'Done',
all: 'All',
inboxTitle: 'Updates Inbox',
inboxTitle: 'Updates Center',
inboxSubtitle: 'Focus on actions, then mark updates done or dismissed.',
needsAction: 'Needs Action',
doneCount: 'Done',
@@ -2118,7 +2118,7 @@ const resources = {
actionRequired: '待处理',
done: '已完成',
all: '全部',
inboxTitle: '更新收件箱',
inboxTitle: '更新中心',
inboxSubtitle: '先处理操作,再将更新标记为完成或忽略。',
needsAction: '需处理',
doneCount: '已完成',
@@ -3344,7 +3344,7 @@ const resources = {
actionRequired: 'Hành động bắt buộc',
done: 'Xong',
all: 'Tất cả',
inboxTitle: 'Hộp thư cập nhật',
inboxTitle: 'Trung tâm cập nhật',
inboxSubtitle:
'Tập trung vào các hành động, sau đó đánh dấu các cập nhật đã hoàn tất hoặc bị loại bỏ.',
needsAction: 'Cần hành động',
@@ -4580,7 +4580,7 @@ const resources = {
actionRequired: '対応が必要',
done: '完了',
all: 'すべて',
inboxTitle: '更新インボックス',
inboxTitle: '更新センター',
inboxSubtitle: '対応を優先し、その後で完了または非表示にします。',
needsAction: '要対応',
doneCount: '完了',
+4 -4
View File
@@ -169,9 +169,9 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
},
{
id: 'updates-center-launch',
title: 'Updates inbox is available for rollout tasks',
title: 'Updates Center is available for rollout tasks',
summary:
'A focused updates inbox exists for setup tasks and rollout guidance when you need it.',
'A focused Updates Center exists for setup tasks and rollout guidance when you need it.',
primaryAction:
'Use this page only when needed for rollout tasks, then return to your normal workflow.',
publishedAt: '2026-02-25',
@@ -186,7 +186,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
actions: [
{
id: 'open-updates-page',
label: 'Open updates inbox when needed',
label: 'Open Updates Center when needed',
description: 'Review rollout tasks only when you want guided setup changes.',
type: 'route',
path: '/updates',
@@ -199,7 +199,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
command: 'ccs config',
},
],
routes: [{ label: 'Updates Inbox', path: '/updates' }],
routes: [{ label: 'Updates Center', path: '/updates' }],
commands: ['ccs config'],
},
];
@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest';
import { render, screen } from '@tests/setup/test-utils';
import { ProviderInfoTab } from '@/components/cliproxy/provider-editor/provider-info-tab';
const authenticatedStatus = {
provider: 'codex',
displayName: 'Codex',
authenticated: true,
lastAuth: null,
tokenFiles: 1,
accounts: [],
};
describe('ProviderInfoTab', () => {
it('routes management commands through Claude target for codex-target variants', () => {
render(
<ProviderInfoTab
provider="codex"
displayName="Codex"
defaultTarget="codex"
authStatus={authenticatedStatus}
supportsModelConfig
/>
);
expect(screen.getByText('ccs codex --target claude --config')).toBeInTheDocument();
expect(screen.getByText('ccs codex --target claude --auth --add')).toBeInTheDocument();
expect(screen.getByText('ccs codex --target claude --accounts')).toBeInTheDocument();
expect(
screen.getByText(
/Codex and Droid runtime launches reject those CLIProxy management commands\./
)
).toBeInTheDocument();
});
it('hides unsupported model-config commands when the provider has no catalog support', () => {
render(
<ProviderInfoTab
provider="custom-provider"
displayName="Custom Provider"
defaultTarget="claude"
authStatus={{
...authenticatedStatus,
provider: 'custom-provider',
displayName: 'Custom Provider',
}}
supportsModelConfig={false}
/>
);
expect(screen.queryByText('Change model')).not.toBeInTheDocument();
expect(screen.getByText('ccs custom-provider --auth --add')).toBeInTheDocument();
});
});
@@ -0,0 +1,89 @@
import { describe, expect, it } from 'vitest';
import { render, screen } from '@tests/setup/test-utils';
import { CodexOverviewTab } from '@/components/compatible-cli/codex-overview-tab';
import type { CodexDashboardDiagnostics } from '@/hooks/use-codex-types';
function buildDiagnostics(activeProfile: string | null): CodexDashboardDiagnostics {
return {
binary: {
installed: true,
path: '/tmp/codex',
installDir: '/tmp',
source: 'PATH',
version: 'codex-cli 0.118.0-alpha.3',
overridePath: null,
supportsConfigOverrides: true,
},
file: {
label: 'Codex user config',
path: '$CODEX_HOME/config.toml',
resolvedPath: '/tmp/.codex/config.toml',
exists: true,
isSymlink: false,
isRegularFile: true,
sizeBytes: 64,
mtimeMs: 100,
parseError: null,
readError: null,
},
workspacePath: '/tmp/workspace',
config: {
model: 'gpt-5.4',
modelReasoningEffort: null,
modelProvider: 'openai',
activeProfile,
approvalPolicy: null,
sandboxMode: null,
webSearch: null,
toolOutputTokenLimit: null,
personality: null,
topLevelKeys: ['model_provider'],
profileCount: activeProfile ? 1 : 0,
profileNames: activeProfile ? [activeProfile] : [],
modelProviderCount: 0,
modelProviders: [],
featureCount: 0,
enabledFeatures: [],
disabledFeatures: [],
trustedProjectCount: 0,
untrustedProjectCount: 0,
projectTrust: [],
mcpServerCount: 0,
mcpServers: [],
},
supportMatrix: [],
warnings: [],
docsReference: {
providerValues: ['openai', 'oss', 'custom model_providers'],
settingsHierarchy: [],
notes: [],
links: [],
providerDocs: [],
},
};
}
describe('CodexOverviewTab', () => {
it('uses a bare codex command when no active profile is set', () => {
render(<CodexOverviewTab diagnostics={buildDiagnostics(null)} />);
expect(screen.getByText('Open native Codex')).toBeInTheDocument();
expect(screen.getAllByText('codex').length).toBeGreaterThan(0);
expect(screen.queryByText('codex --profile default')).not.toBeInTheDocument();
expect(
screen.getAllByText(
(_, node) =>
node?.textContent?.includes(
'Built-in openai and oss providers are also valid native defaults'
) ?? false
).length
).toBeGreaterThan(0);
});
it('uses the active named profile when one is selected', () => {
render(<CodexOverviewTab diagnostics={buildDiagnostics('work')} />);
expect(screen.getByText('Inspect active profile')).toBeInTheDocument();
expect(screen.getByText('codex --profile work')).toBeInTheDocument();
});
});
@@ -22,4 +22,18 @@ describe('support-updates catalog codex routing', () => {
expect(entry).toBeDefined();
expect(entry?.routes).toEqual([{ label: 'Codex CLI', path: '/codex' }]);
});
it('uses Updates Center naming consistently for the rollout notice', () => {
const notice = SUPPORT_NOTICES.find((entry) => entry.id === 'updates-center-launch');
expect(notice).toBeDefined();
expect(notice?.title).toContain('Updates Center');
expect(notice?.actions).toContainEqual(
expect.objectContaining({
id: 'open-updates-page',
label: 'Open Updates Center when needed',
})
);
expect(notice?.routes).toContainEqual({ label: 'Updates Center', path: '/updates' });
});
});