diff --git a/docs/code-standards.md b/docs/code-standards.md index 76eae3d6..7e45c745 100644 --- a/docs/code-standards.md +++ b/docs/code-standards.md @@ -198,16 +198,21 @@ Resolves which adapter to use via `resolveTargetType()`: ``` 1. --target 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 diff --git a/docs/codebase-summary.md b/docs/codebase-summary.md index d682c11a..3e3e1607 100644 --- a/docs/codebase-summary.md +++ b/docs/codebase-summary.md @@ -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 ` 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` diff --git a/docs/system-architecture/index.md b/docs/system-architecture/index.md index ce103371..338934b4 100644 --- a/docs/system-architecture/index.md +++ b/docs/system-architecture/index.md @@ -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). diff --git a/docs/system-architecture/target-adapters.md b/docs/system-architecture/target-adapters.md index d78ae7e9..9371ab72 100644 --- a/docs/system-architecture/target-adapters.md +++ b/docs/system-architecture/target-adapters.md @@ -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 diff --git a/src/bin/ccsxp-runtime.ts b/src/bin/ccsxp-runtime.ts index 6758e655..f488e5fb 100644 --- a/src/bin/ccsxp-runtime.ts +++ b/src/bin/ccsxp-runtime.ts @@ -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'); diff --git a/src/commands/config-channels-command.ts b/src/commands/config-channels-command.ts index 6ccf49d9..b06c654d 100644 --- a/src/commands/config-channels-command.ts +++ b/src/commands/config-channels-command.ts @@ -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(''); } diff --git a/src/commands/config-command-options.ts b/src/commands/config-command-options.ts index 24d31a38..f219136d 100644 --- a/src/commands/config-command-options.ts +++ b/src/commands/config-command-options.ts @@ -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 Save channel token (telegram= or discord=)'); console.log(' --clear-token Remove all saved channel tokens'); console.log(' --clear-token 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'); diff --git a/src/commands/help-command.ts b/src/commands/help-command.ts index c1e878ba..a30e6829 100644 --- a/src/commands/help-command.ts +++ b/src/commands/help-command.ts @@ -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//.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.'], diff --git a/src/web-server/services/codex-dashboard-service.ts b/src/web-server/services/codex-dashboard-service.ts index 3285f29b..ce95894f 100644 --- a/src/web-server/services/codex-dashboard-service.ts +++ b/src/web-server/services/codex-dashboard-service.ts @@ -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 { 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 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.`); } } diff --git a/tests/unit/commands/help-command-parity.test.ts b/tests/unit/commands/help-command-parity.test.ts index 687fd2e5..08397323 100644 --- a/tests/unit/commands/help-command-parity.test.ts +++ b/tests/unit/commands/help-command-parity.test.ts @@ -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); diff --git a/tests/unit/targets/codex-runtime-integration.test.ts b/tests/unit/targets/codex-runtime-integration.test.ts index 3c186354..7e06621d 100644 --- a/tests/unit/targets/codex-runtime-integration.test.ts +++ b/tests/unit/targets/codex-runtime-integration.test.ts @@ -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; diff --git a/tests/unit/web-server/codex-dashboard-service.test.ts b/tests/unit/web-server/codex-dashboard-service.test.ts index 08855f0e..2700622b 100644 --- a/tests/unit/web-server/codex-dashboard-service.test.ts +++ b/tests/unit/web-server/codex-dashboard-service.test.ts @@ -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'), diff --git a/ui/src/components/cliproxy/provider-editor/index.tsx b/ui/src/components/cliproxy/provider-editor/index.tsx index 55d858f0..bee323a6 100644 --- a/ui/src/components/cliproxy/provider-editor/index.tsx +++ b/ui/src/components/cliproxy/provider-editor/index.tsx @@ -297,6 +297,7 @@ export function ProviderEditor({ defaultTarget={defaultTarget} data={data} authStatus={authStatus} + supportsModelConfig={Boolean(catalog)} /> diff --git a/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx b/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx index 4d2c30f2..d8af6ad2 100644 --- a/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx +++ b/ui/src/components/cliproxy/provider-editor/provider-info-tab.tsx @@ -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 ( @@ -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"`} /> - - - + {supportsModelConfig && ( + + )} + + + {resolvedTarget !== 'claude' && ( +

+ Account and model-management flags stay on Claude target. Codex and Droid runtime + launches reject those CLIProxy management commands. +

+ )} diff --git a/ui/src/components/compatible-cli/codex-docs-tab.tsx b/ui/src/components/compatible-cli/codex-docs-tab.tsx index 6cce782d..1474155d 100644 --- a/ui/src/components/compatible-cli/codex-docs-tab.tsx +++ b/ui/src/components/compatible-cli/codex-docs-tab.tsx @@ -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[] = []; diff --git a/ui/src/components/compatible-cli/codex-overview-tab.tsx b/ui/src/components/compatible-cli/codex-overview-tab.tsx index accf30fb..ee0bcda3 100644 --- a/ui/src/components/compatible-cli/codex-overview-tab.tsx +++ b/ui/src/components/compatible-cli/codex-overview-tab.tsx @@ -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 (
@@ -88,6 +86,10 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { [model_providers.cliproxy] entry if you want CLIProxy as the saved native default.

+

+ Built-in openai and oss providers are also valid native + defaults and do not need a custom [model_providers] stanza. +

Saved default targets for API profiles and variants still remain on Claude or Droid.

@@ -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.', }, ]} /> diff --git a/ui/src/lib/codex-config.ts b/ui/src/lib/codex-config.ts index b13a16b8..cdc0b386 100644 --- a/ui/src/lib/codex-config.ts +++ b/ui/src/lib/codex-config.ts @@ -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', diff --git a/ui/src/lib/i18n.ts b/ui/src/lib/i18n.ts index f825d60f..8038a835 100644 --- a/ui/src/lib/i18n.ts +++ b/ui/src/lib/i18n.ts @@ -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: '完了', diff --git a/ui/src/lib/support-updates-catalog.ts b/ui/src/lib/support-updates-catalog.ts index c67dadf4..33c48e2d 100644 --- a/ui/src/lib/support-updates-catalog.ts +++ b/ui/src/lib/support-updates-catalog.ts @@ -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'], }, ]; diff --git a/ui/tests/unit/components/cliproxy/provider-editor/provider-info-tab.test.tsx b/ui/tests/unit/components/cliproxy/provider-editor/provider-info-tab.test.tsx new file mode 100644 index 00000000..9d71278d --- /dev/null +++ b/ui/tests/unit/components/cliproxy/provider-editor/provider-info-tab.test.tsx @@ -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( + + ); + + 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( + + ); + + expect(screen.queryByText('Change model')).not.toBeInTheDocument(); + expect(screen.getByText('ccs custom-provider --auth --add')).toBeInTheDocument(); + }); +}); diff --git a/ui/tests/unit/components/compatible-cli/codex-overview-tab.test.tsx b/ui/tests/unit/components/compatible-cli/codex-overview-tab.test.tsx new file mode 100644 index 00000000..ed8c8bac --- /dev/null +++ b/ui/tests/unit/components/compatible-cli/codex-overview-tab.test.tsx @@ -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(); + + 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(); + + expect(screen.getByText('Inspect active profile')).toBeInTheDocument(); + expect(screen.getByText('codex --profile work')).toBeInTheDocument(); + }); +}); diff --git a/ui/tests/unit/ui/lib/support-updates-catalog.test.ts b/ui/tests/unit/ui/lib/support-updates-catalog.test.ts index b7e69f84..85410d35 100644 --- a/ui/tests/unit/ui/lib/support-updates-catalog.test.ts +++ b/ui/tests/unit/ui/lib/support-updates-catalog.test.ts @@ -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' }); + }); });