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) 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 - ccs-droid → droid
- ccsd → droid - ccsd → droid
- ccs-codex → codex - ccs-codex → codex
- ccsx → codex - ccsx → codex
- ccs → default - 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 ### Registration Pattern
+9 -5
View File
@@ -36,8 +36,9 @@ The main CLI is organized into domain-specific modules with barrel exports.
src/ src/
├── ccs.ts # Main entry point & profile execution flow ├── ccs.ts # Main entry point & profile execution flow
├── bin/ # Dedicated runtime entrypoints ├── bin/ # Dedicated runtime entrypoints
│ ├── droid-runtime.ts # argv[0] shim for ccs-droid / ccsd │ ├── droid-runtime.ts # Forces droid target for ccs-droid / ccsd package bins
── codex-runtime.ts # argv[0] shim for ccs-codex / ccsx ── 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 ├── types/ # TypeScript type definitions
│ ├── index.ts # Barrel export (aggregates all types) │ ├── index.ts # Barrel export (aggregates all types)
│ ├── cli.ts # CLI types (ParsedArgs, ExitCode) │ ├── cli.ts # CLI types (ParsedArgs, ExitCode)
@@ -69,7 +70,7 @@ src/
│ ├── index.ts # Barrel export │ ├── index.ts # Barrel export
│ ├── target-adapter.ts # TargetAdapter interface contract │ ├── target-adapter.ts # TargetAdapter interface contract
│ ├── target-registry.ts # Registry for runtime adapter lookup │ ├── 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-metadata.ts # Runtime vs persisted target metadata and alias lists
│ ├── target-runtime-compatibility.ts # Guardrails for target/profile combinations │ ├── target-runtime-compatibility.ts # Guardrails for target/profile combinations
│ ├── claude-adapter.ts # Claude Code CLI implementation │ ├── claude-adapter.ts # Claude Code CLI implementation
@@ -248,7 +249,9 @@ src/
### Native Codex Runtime Target ### 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`. - 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. - 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. - 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: 2. **Target Resolution** - Priority order:
- `--target <cli>` flag (CLI argument) - `--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) - Per-profile `target` field (from config.yaml)
- Default: `claude` - 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) Profile Resolution (CLIProxy, Settings/API, Account-based)
| |
v v
Target Resolution (--target flag > argv[0] > config > default) Target Resolution (--target flag > runtime entrypoint / argv[0] > config > default)
| |
v v
Get Target Adapter (Claude, Droid, or Codex) Get Target Adapter (Claude, Droid, or Codex)
@@ -91,7 +91,7 @@ Spawn Target Process
- Preserves native `~/.codex/config.toml` ownership - Preserves native `~/.codex/config.toml` ownership
- Dashboard page reads/writes only the user config layer with explicit runtime-vs-provider warnings - 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) ccs → Target: claude (default)
@@ -99,6 +99,7 @@ ccs-droid → Target: droid (explicit alias)
ccsd → Target: droid (legacy shortcut) ccsd → Target: droid (legacy shortcut)
ccs-codex → Target: codex (explicit alias) ccs-codex → Target: codex (explicit alias)
ccsx → Target: codex (short 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). 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 droid glm
└─ ccs --target codex └─ 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 └─ ccs-droid (explicit alias) → droid
└─ ccsd (legacy shortcut) → droid └─ ccsd (legacy shortcut) → droid
└─ ccs-codex (explicit alias) → codex └─ ccs-codex (explicit alias) → codex
└─ ccsx (short alias) → codex └─ ccsx (short alias) → codex
└─ ccs (regular command) → default └─ 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` └─ persisted targets are currently only `claude` and `droid`
└─ profiles: └─ profiles:
glm: glm:
target: droid target: droid
4. Fallback: 'claude' — lowest priority 5. Fallback: 'claude' — lowest priority
``` ```
### Implementation ### Implementation
@@ -117,19 +122,25 @@ export function resolveTargetType(
return parsed.targetOverride; 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, ''); const binName = path.basename(process.argv[1] || process.argv0 || '').replace(/\.(cmd|bat|ps1|exe)$/i, '');
if (ARGV0_TARGET_MAP[binName]) { if (ARGV0_TARGET_MAP[binName]) {
return ARGV0_TARGET_MAP[binName]; return ARGV0_TARGET_MAP[binName];
} }
// 3. Check profile config // 4. Check profile config
if (profileConfig?.target) { if (profileConfig?.target) {
// Persisted targets intentionally exclude runtime-only codex. // Persisted targets intentionally exclude runtime-only codex.
return profileConfig.target; return profileConfig.target;
} }
// 4. Default to claude // 5. Default to claude
return '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 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. one place to inspect and manage the user-owned layer safely.
### Runtime Alias Pattern ### Runtime Entrypoints and argv[0] Fallback
```bash ```bash
# Built-in package bin aliases # Built-in package bin entrypoints
ccs-codex ccs-codex
Target: codex (forced by runtime alias) dist/bin/codex-runtime.js
CCS_INTERNAL_ENTRY_TARGET=codex
ccsx codex ccsx
Target: codex (forced by runtime alias) dist/bin/codex-runtime.js
→ codex ...args 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 If a user launches CCS through a custom shim instead of the built-in package bins, target
`CCS_CODEX_ALIASES` after creating a matching launcher: resolution falls back to `argv[0]` aliases from `CCS_TARGET_ALIASES` or legacy
`CCS_CODEX_ALIASES`:
```bash ```bash
ln -s /path/to/ccs /path/to/mycodex ln -s /path/to/ccs /path/to/mycodex
@@ -757,6 +775,7 @@ ccs --target droid help
# Test Codex adapter (if installed) # Test Codex adapter (if installed)
ccs --target codex ccs --target codex
ccs-codex ccs-codex
ccsxp
# Test argv[0] detection # Test argv[0] detection
ccs-droid help ccs-droid help
+11 -1
View File
@@ -1,9 +1,19 @@
const { stripTargetFlag } = require('../targets/target-resolver'); const { stripTargetFlag } = require('../targets/target-resolver');
const { fail } = require('../utils/ui');
process.env.CCS_INTERNAL_ENTRY_TARGET = 'codex'; process.env.CCS_INTERNAL_ENTRY_TARGET = 'codex';
// ccsxp is an opinionated shortcut for the built-in Codex-on-Codex route. // ccsxp is an opinionated shortcut for the built-in Codex-on-Codex route.
// Strip user-supplied target overrides before forcing the shortcut target. // 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); process.argv.splice(2, process.argv.length - 2, 'codex', '--target', 'codex', ...forwardedArgs);
require('../ccs'); require('../ccs');
+1 -3
View File
@@ -177,9 +177,7 @@ function showHelp(): void {
console.log( console.log(
` $ ${color('ccs config channels --clear-token discord', 'command')} ${dim('# Clear one token')}` ` $ ${color('ccs config channels --clear-token discord', 'command')} ${dim('# Clear one token')}`
); );
console.log( console.log(` ${dim(getOfficialChannelsSupportMessage())}`);
` ${dim('Official Channels only work on native Claude default/account sessions, not on ccs glm or other API/OAuth/Droid targets.')}`
);
console.log(''); console.log('');
} }
+2 -2
View File
@@ -1,4 +1,5 @@
import { extractOption, hasAnyFlag, scanCommandArgs } from './arg-extractor'; 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; 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(' --set-token <s> Save channel token (telegram=<t> or discord=<t>)');
console.log(' --clear-token Remove all saved channel tokens'); console.log(' --clear-token Remove all saved channel tokens');
console.log(' --clear-token <c> Remove one saved channel token'); console.log(' --clear-token <c> Remove one saved channel token');
console.log(' Works only for native Claude default/account sessions'); console.log(` ${getOfficialChannelsSupportMessage()}`);
console.log(' Not for ccs glm, other API/OAuth profiles, or Droid targets');
console.log(''); console.log('');
console.log(' auth Manage dashboard authentication'); console.log(' auth Manage dashboard authentication');
console.log(' auth setup Configure username and password'); 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 { isUnifiedMode } from '../config/unified-config-loader';
import { getCcsDir, getCcsDirSource } from '../utils/config-manager'; import { getCcsDir, getCcsDirSource } from '../utils/config-manager';
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager'; import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
import { getOfficialChannelsSupportMessage } from '../channels/official-channels-runtime';
type HelpWriter = (line: string) => void; 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'], ['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.'], ['', '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.'], ['Note:', getOfficialChannelsSupportMessage()],
['', 'Not supported for ccs glm, other API/OAuth profiles, or Droid targets.'],
['', 'Telegram/Discord tokens live in ~/.claude/channels/<channel>/.env.'], ['', 'Telegram/Discord tokens live in ~/.claude/channels/<channel>/.env.'],
['', 'Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work for that launch.'], ['', '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.'], ['', '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 WEB_SEARCH_VALUES = new Set(['cached', 'live', 'disabled']);
const PERSONALITY_VALUES = new Set(['none', 'friendly', 'pragmatic']); const PERSONALITY_VALUES = new Set(['none', 'friendly', 'pragmatic']);
const PROJECT_TRUST_LEVEL_VALUES = new Set(['trusted', 'untrusted']); 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> { function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value); 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)); .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): { export function summarizeCodexFeatureFlags(value: unknown): {
all: CodexFeatureFlagDiagnostics[]; all: CodexFeatureFlagDiagnostics[];
enabled: CodexFeatureFlagDiagnostics[]; enabled: CodexFeatureFlagDiagnostics[];
@@ -729,15 +734,15 @@ export async function getCodexDashboardDiagnostics(): Promise<CodexDashboardDiag
} }
if (activeModelProvider) { if (activeModelProvider) {
const activeProvider = modelProviders.find((provider) => provider.name === activeModelProvider); const activeProvider = modelProviders.find((provider) => provider.name === activeModelProvider);
if (!activeProvider) { if (!activeProvider && !isBuiltInCodexModelProvider(activeModelProvider)) {
warnings.push( warnings.push(
`Active model provider "${activeModelProvider}" is selected but missing from [model_providers].` `Active model provider "${activeModelProvider}" is selected but missing from [model_providers].`
); );
} else { } else if (activeProvider) {
if (!activeProvider.baseUrl) { if (!activeProvider.baseUrl) {
warnings.push(`Active model provider "${activeProvider.name}" is missing base_url.`); 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.`); 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.' 'Fastest path: turn on the channel, save the token if needed, then run ccs.'
) )
).toBe(true); ).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( expect(
rendered.includes('Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work') rendered.includes('Current-process TELEGRAM_BOT_TOKEN / DISCORD_BOT_TOKEN also work')
).toBe(true); ).toBe(true);
@@ -278,6 +278,22 @@ process.exit(0);
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['--version']]); 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', () => { it('passes ccs codex --target codex --version through to the native Codex binary', () => {
if (process.platform === 'win32') return; if (process.platform === 'win32') return;
@@ -234,6 +234,32 @@ wire_api = "responses"
expect(diagnostics.warnings.some((warning) => warning.includes('missing env_key'))).toBe(true); 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 () => { it('summarizes granular approval policies without flattening them to null', async () => {
fs.writeFileSync( fs.writeFileSync(
path.join(codexHome, 'config.toml'), path.join(codexHome, 'config.toml'),
@@ -297,6 +297,7 @@ export function ProviderEditor({
defaultTarget={defaultTarget} defaultTarget={defaultTarget}
data={data} data={data}
authStatus={authStatus} authStatus={authStatus}
supportsModelConfig={Boolean(catalog)}
/> />
</TabsContent> </TabsContent>
</div> </div>
@@ -17,6 +17,7 @@ interface ProviderInfoTabProps {
defaultTarget?: CliTarget; defaultTarget?: CliTarget;
data?: SettingsResponse; data?: SettingsResponse;
authStatus: AuthStatus; authStatus: AuthStatus;
supportsModelConfig?: boolean;
} }
export function ProviderInfoTab({ export function ProviderInfoTab({
@@ -25,10 +26,16 @@ export function ProviderInfoTab({
defaultTarget, defaultTarget,
data, data,
authStatus, authStatus,
supportsModelConfig = false,
}: ProviderInfoTabProps) { }: ProviderInfoTabProps) {
const resolvedTarget = defaultTarget || 'claude'; const resolvedTarget = defaultTarget || 'claude';
const isDroidTarget = resolvedTarget === 'droid'; const isDroidTarget = resolvedTarget === 'droid';
const isCodexProvider = provider === 'codex'; 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 ( return (
<ScrollArea className="h-full"> <ScrollArea className="h-full">
@@ -109,9 +116,30 @@ export function ProviderInfoTab({
label={isDroidTarget ? 'Override to Claude' : 'Run on Droid (--target)'} label={isDroidTarget ? 'Override to Claude' : 'Run on Droid (--target)'}
command={`ccs ${provider} --target ${isDroidTarget ? 'claude' : 'droid'} "your prompt"`} command={`ccs ${provider} --target ${isDroidTarget ? 'claude' : 'droid'} "your prompt"`}
/> />
<UsageCommand label="Change model" command={`ccs ${provider} --config`} /> {supportsModelConfig && (
<UsageCommand label="Add account" command={`ccs ${provider} --add`} /> <UsageCommand
<UsageCommand label="List accounts" command={`ccs ${provider} --accounts`} /> 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> </div>
</div> </div>
@@ -7,6 +7,7 @@ import type {
CompatibleCliProviderDocLink, CompatibleCliProviderDocLink,
CodexDashboardDiagnostics, CodexDashboardDiagnostics,
} from '@/hooks/use-codex-types'; } from '@/hooks/use-codex-types';
import { CLIPROXY_NATIVE_CODEX_RECIPE } from '@/lib/codex-config';
const DEFAULT_CODEX_DOC_LINKS = [ 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[] { function renderTextWithLinks(text: string): ReactNode[] {
const urlPattern = /https?:\/\/[^\s)]+/g; const urlPattern = /https?:\/\/[^\s)]+/g;
const nodes: ReactNode[] = []; const nodes: ReactNode[] = [];
@@ -22,15 +22,9 @@ import {
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import type { CodexDashboardDiagnostics } from '@/hooks/use-codex-types'; import type { CodexDashboardDiagnostics } from '@/hooks/use-codex-types';
import { CLIPROXY_NATIVE_CODEX_RECIPE } from '@/lib/codex-config';
import { cn } from '@/lib/utils'; 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 { function formatTimestamp(value: number | null | undefined): string {
if (!value || !Number.isFinite(value)) return 'N/A'; if (!value || !Number.isFinite(value)) return 'N/A';
return new Date(value).toLocaleString(); return new Date(value).toLocaleString();
@@ -65,6 +59,10 @@ interface CodexOverviewTabProps {
} }
export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
const inspectProfileCommand = diagnostics.config.activeProfile
? `codex --profile ${diagnostics.config.activeProfile}`
: 'codex';
return ( return (
<ScrollArea className="h-full"> <ScrollArea className="h-full">
<div className="space-y-4 pr-1"> <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 <code>[model_providers.cliproxy]</code> entry if you want CLIProxy as the saved native
default. default.
</p> </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> <p>
Saved default targets for API profiles and variants still remain on Claude or Droid. Saved default targets for API profiles and variants still remain on Claude or Droid.
</p> </p>
@@ -281,9 +283,13 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
description: 'Use the explicit built-in Codex provider route.', description: 'Use the explicit built-in Codex provider route.',
}, },
{ {
label: 'Workspace trust', label: diagnostics.config.activeProfile
command: `codex --profile ${diagnostics.config.activeProfile || 'default'}`, ? 'Inspect active profile'
description: 'Inspect the active profile directly in native Codex.', : '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; 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[] = [ export const KNOWN_CODEX_FEATURES: CodexFeatureCatalogEntry[] = [
{ {
name: 'multi_agent', name: 'multi_agent',
+4 -4
View File
@@ -950,7 +950,7 @@ const resources = {
actionRequired: 'Action Required', actionRequired: 'Action Required',
done: 'Done', done: 'Done',
all: 'All', all: 'All',
inboxTitle: 'Updates Inbox', inboxTitle: 'Updates Center',
inboxSubtitle: 'Focus on actions, then mark updates done or dismissed.', inboxSubtitle: 'Focus on actions, then mark updates done or dismissed.',
needsAction: 'Needs Action', needsAction: 'Needs Action',
doneCount: 'Done', doneCount: 'Done',
@@ -2118,7 +2118,7 @@ const resources = {
actionRequired: '待处理', actionRequired: '待处理',
done: '已完成', done: '已完成',
all: '全部', all: '全部',
inboxTitle: '更新收件箱', inboxTitle: '更新中心',
inboxSubtitle: '先处理操作,再将更新标记为完成或忽略。', inboxSubtitle: '先处理操作,再将更新标记为完成或忽略。',
needsAction: '需处理', needsAction: '需处理',
doneCount: '已完成', doneCount: '已完成',
@@ -3344,7 +3344,7 @@ const resources = {
actionRequired: 'Hành động bắt buộc', actionRequired: 'Hành động bắt buộc',
done: 'Xong', done: 'Xong',
all: 'Tất cả', all: 'Tất cả',
inboxTitle: 'Hộp thư cập nhật', inboxTitle: 'Trung tâm cập nhật',
inboxSubtitle: 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ỏ.', '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', needsAction: 'Cần hành động',
@@ -4580,7 +4580,7 @@ const resources = {
actionRequired: '対応が必要', actionRequired: '対応が必要',
done: '完了', done: '完了',
all: 'すべて', all: 'すべて',
inboxTitle: '更新インボックス', inboxTitle: '更新センター',
inboxSubtitle: '対応を優先し、その後で完了または非表示にします。', inboxSubtitle: '対応を優先し、その後で完了または非表示にします。',
needsAction: '要対応', needsAction: '要対応',
doneCount: '完了', doneCount: '完了',
+4 -4
View File
@@ -169,9 +169,9 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
}, },
{ {
id: 'updates-center-launch', id: 'updates-center-launch',
title: 'Updates inbox is available for rollout tasks', title: 'Updates Center is available for rollout tasks',
summary: 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: primaryAction:
'Use this page only when needed for rollout tasks, then return to your normal workflow.', 'Use this page only when needed for rollout tasks, then return to your normal workflow.',
publishedAt: '2026-02-25', publishedAt: '2026-02-25',
@@ -186,7 +186,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
actions: [ actions: [
{ {
id: 'open-updates-page', 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.', description: 'Review rollout tasks only when you want guided setup changes.',
type: 'route', type: 'route',
path: '/updates', path: '/updates',
@@ -199,7 +199,7 @@ export const SUPPORT_NOTICES: SupportNotice[] = [
command: 'ccs config', command: 'ccs config',
}, },
], ],
routes: [{ label: 'Updates Inbox', path: '/updates' }], routes: [{ label: 'Updates Center', path: '/updates' }],
commands: ['ccs config'], 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).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', () => {
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' });
});
}); });