diff --git a/src/ccs.ts b/src/ccs.ts index 457536dd..ba962e88 100644 --- a/src/ccs.ts +++ b/src/ccs.ts @@ -87,6 +87,11 @@ interface DetectedProfile { remainingArgs: string[]; } +interface RuntimeReasoningResolution { + argsWithoutReasoningFlags: string[]; + reasoningOverride: string | number | undefined; +} + /** * Smart profile detection */ @@ -100,6 +105,42 @@ function detectProfile(args: string[]): DetectedProfile { } } +function resolveRuntimeReasoningFlags( + args: string[], + envThinkingValue: string | undefined +): RuntimeReasoningResolution { + const runtime = resolveDroidReasoningRuntime(args, envThinkingValue); + + if (runtime.duplicateDisplays.length > 0) { + console.error( + warn( + `[!] Multiple reasoning flags detected. Using first occurrence: ${runtime.sourceDisplay || ''}` + ) + ); + } + + return { + argsWithoutReasoningFlags: runtime.argsWithoutReasoningFlags, + reasoningOverride: runtime.reasoningOverride, + }; +} + +function exitWithRuntimeReasoningFlagError( + message: string, + options: { + codexAliasLevels: string; + includeDroidExecExample?: boolean; + } +): never { + console.error(fail(message)); + console.error(' Examples: --thinking low, --thinking 8192, --thinking off'); + console.error(` Codex alias: --effort ${options.codexAliasLevels}`); + if (options.includeDroidExecExample) { + console.error(' Droid exec: --reasoning-effort high'); + } + process.exit(1); +} + // ========== Main Execution ========== interface ProfileError extends Error { @@ -463,17 +504,9 @@ async function main(): Promise { targetRemainingArgs = droidRoute.argsForDroid; if (droidRoute.mode === 'interactive') { - const runtime = resolveDroidReasoningRuntime(remainingArgs, process.env.CCS_THINKING); + const runtime = resolveRuntimeReasoningFlags(remainingArgs, process.env.CCS_THINKING); targetRemainingArgs = runtime.argsWithoutReasoningFlags; runtimeReasoningOverride = runtime.reasoningOverride; - - if (runtime.duplicateDisplays.length > 0) { - console.error( - warn( - `[!] Multiple reasoning flags detected. Using first occurrence: ${runtime.sourceDisplay || ''}` - ) - ); - } } else { if (droidRoute.duplicateReasoningDisplays.length > 0) { console.error( @@ -490,33 +523,23 @@ async function main(): Promise { } } catch (error) { if (error instanceof DroidReasoningFlagError || error instanceof DroidCommandRouterError) { - console.error(fail(error.message)); - console.error(' Examples: --thinking low, --thinking 8192, --thinking off'); - console.error(' Codex alias: --effort medium|high|xhigh'); - console.error(' Droid exec: --reasoning-effort high'); - process.exit(1); + exitWithRuntimeReasoningFlagError(error.message, { + codexAliasLevels: 'medium|high|xhigh', + includeDroidExecExample: true, + }); } throw error; } } else if (resolvedTarget === 'codex') { try { - const runtime = resolveDroidReasoningRuntime(remainingArgs, process.env.CCS_THINKING); + const runtime = resolveRuntimeReasoningFlags(remainingArgs, process.env.CCS_THINKING); targetRemainingArgs = runtime.argsWithoutReasoningFlags; runtimeReasoningOverride = runtime.reasoningOverride; - - if (runtime.duplicateDisplays.length > 0) { - console.error( - warn( - `[!] Multiple reasoning flags detected. Using first occurrence: ${runtime.sourceDisplay || ''}` - ) - ); - } } catch (error) { if (error instanceof DroidReasoningFlagError) { - console.error(fail(error.message)); - console.error(' Examples: --thinking low, --thinking 8192, --thinking off'); - console.error(' Codex alias: --effort minimal|low|medium|high|xhigh'); - process.exit(1); + exitWithRuntimeReasoningFlagError(error.message, { + codexAliasLevels: 'minimal|low|medium|high|xhigh', + }); } throw error; } diff --git a/src/targets/codex-adapter.ts b/src/targets/codex-adapter.ts index 1fb24b60..eedaf554 100644 --- a/src/targets/codex-adapter.ts +++ b/src/targets/codex-adapter.ts @@ -229,6 +229,8 @@ export class CodexAdapter implements TargetAdapter { } supportsProfileType(profileType: ProfileType): boolean { - return profileType === 'default' || profileType === 'settings' || profileType === 'cliproxy'; + // Bridge-backed settings profiles need additional compatibility context that the + // adapter contract does not receive, so keep the adapter-level claim conservative. + return profileType === 'default' || profileType === 'cliproxy'; } } diff --git a/src/targets/target-metadata.ts b/src/targets/target-metadata.ts index af28aeb4..1c0ae30b 100644 --- a/src/targets/target-metadata.ts +++ b/src/targets/target-metadata.ts @@ -25,7 +25,7 @@ export const TARGET_METADATA: Record = { legacyAliasEnvVar: 'CCS_CODEX_ALIASES', persistedTarget: false, }, -}; +} satisfies Record; export const RUNTIME_TARGET_TYPES = Object.freeze( Object.keys(TARGET_METADATA) as TargetType[] diff --git a/tests/unit/targets/codex-adapter.test.ts b/tests/unit/targets/codex-adapter.test.ts index d74b384b..0889219b 100644 --- a/tests/unit/targets/codex-adapter.test.ts +++ b/tests/unit/targets/codex-adapter.test.ts @@ -5,10 +5,10 @@ import { CodexAdapter } from '../../../src/targets/codex-adapter'; describe('CodexAdapter', () => { const adapter = new CodexAdapter(); - test('supports default, settings, and cliproxy profile types', () => { + test('supports only adapter-level default and cliproxy profile types', () => { expect(adapter.supportsProfileType('default')).toBe(true); - expect(adapter.supportsProfileType('settings')).toBe(true); expect(adapter.supportsProfileType('cliproxy')).toBe(true); + expect(adapter.supportsProfileType('settings')).toBe(false); expect(adapter.supportsProfileType('account')).toBe(false); expect(adapter.supportsProfileType('copilot')).toBe(false); }); diff --git a/tests/unit/targets/target-runtime-compatibility.test.ts b/tests/unit/targets/target-runtime-compatibility.test.ts index cc169ec6..e75a0600 100644 --- a/tests/unit/targets/target-runtime-compatibility.test.ts +++ b/tests/unit/targets/target-runtime-compatibility.test.ts @@ -60,6 +60,13 @@ describe('evaluateTargetRuntimeCompatibility', () => { }); expect(compatibility.supported).toBe(false); expect(compatibility.reason).toMatch(/only supports CLIProxy Codex bridge profiles/); + + const genericSettingsCompatibility = evaluateTargetRuntimeCompatibility({ + target: 'codex', + profileType: 'settings', + }); + expect(genericSettingsCompatibility.supported).toBe(false); + expect(genericSettingsCompatibility.reason).toMatch(/currently supports native default sessions/); }); test('rejects account and copilot profiles on Codex target', () => { diff --git a/ui/src/hooks/use-codex-types.ts b/ui/src/hooks/use-codex-types.ts new file mode 100644 index 00000000..64ddba94 --- /dev/null +++ b/ui/src/hooks/use-codex-types.ts @@ -0,0 +1,128 @@ +export interface CompatibleCliDocLink { + id: string; + label: string; + url: string; + category: 'overview' | 'configuration' | 'byok' | 'reference'; + source: 'factory' | 'provider' | 'openai' | 'github'; + description: string; +} + +export interface CompatibleCliProviderDocLink { + provider: string; + label: string; + apiFormat: string; + url: string; +} + +export interface CompatibleCliDocsReference { + providerValues: string[]; + settingsHierarchy: string[]; + notes: string[]; + links: CompatibleCliDocLink[]; + providerDocs: CompatibleCliProviderDocLink[]; +} + +export interface CodexBinaryDiagnostics { + installed: boolean; + path: string | null; + installDir: string | null; + source: 'CCS_CODEX_PATH' | 'PATH' | 'missing'; + version: string | null; + overridePath: string | null; + supportsConfigOverrides: boolean; +} + +export interface CodexConfigFileDiagnostics { + label: string; + path: string; + resolvedPath: string; + exists: boolean; + isSymlink: boolean; + isRegularFile: boolean; + sizeBytes: number | null; + mtimeMs: number | null; + parseError: string | null; + readError: string | null; +} + +export interface CodexModelProviderDiagnostics { + name: string; + baseUrl: string | null; + envKey: string | null; + wireApi: string | null; + requiresOpenaiAuth: boolean; + supportsWebsockets: boolean; + hasQueryParams: boolean; + hasHttpHeaders: boolean; + usesExperimentalBearerToken: boolean; +} + +export interface CodexFeatureFlagDiagnostics { + name: string; + state: 'enabled' | 'disabled' | 'custom'; +} + +export interface CodexProjectTrustDiagnostics { + path: string; + trustLevel: string; +} + +export interface CodexMcpServerDiagnostics { + name: string; + transport: 'stdio' | 'streamable-http' | 'unknown'; + enabled: boolean; + required: boolean; + startupTimeoutSec: number | null; + toolTimeoutSec: number | null; + enabledToolsCount: number; + disabledToolsCount: number; + usesInlineBearerToken: boolean; +} + +export interface CodexSupportMatrixEntry { + id: string; + label: string; + supported: boolean; + notes: string; +} + +export interface CodexUserConfigDiagnostics { + model: string | null; + modelProvider: string | null; + activeProfile: string | null; + approvalPolicy: string | null; + sandboxMode: string | null; + webSearch: string | null; + topLevelKeys: string[]; + profileCount: number; + profileNames: string[]; + modelProviderCount: number; + modelProviders: CodexModelProviderDiagnostics[]; + featureCount: number; + enabledFeatures: CodexFeatureFlagDiagnostics[]; + disabledFeatures: CodexFeatureFlagDiagnostics[]; + trustedProjectCount: number; + untrustedProjectCount: number; + projectTrust: CodexProjectTrustDiagnostics[]; + mcpServerCount: number; + mcpServers: CodexMcpServerDiagnostics[]; +} + +export interface CodexDashboardDiagnostics { + binary: CodexBinaryDiagnostics; + file: CodexConfigFileDiagnostics; + config: CodexUserConfigDiagnostics; + supportMatrix: CodexSupportMatrixEntry[]; + warnings: string[]; + docsReference: CompatibleCliDocsReference; +} + +export interface CodexRawConfigResponse { + path: string; + resolvedPath: string; + exists: boolean; + mtime: number; + rawText: string; + config: Record | null; + parseError: string | null; +} diff --git a/ui/src/hooks/use-codex.ts b/ui/src/hooks/use-codex.ts index f486d571..ae37799d 100644 --- a/ui/src/hooks/use-codex.ts +++ b/ui/src/hooks/use-codex.ts @@ -2,10 +2,7 @@ import { useMemo } from 'react'; import { parse as parseToml } from 'smol-toml'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { ApiConflictError, withApiBase } from '@/lib/api-client'; -import type { - CodexDashboardDiagnostics, - CodexRawConfigResponse, -} from '../../../src/web-server/services/compatible-cli-types'; +import type { CodexDashboardDiagnostics, CodexRawConfigResponse } from './use-codex-types'; type CodexRawConfig = CodexRawConfigResponse;