Files
ccs/ui/src/lib/codex-config.ts
T

273 lines
8.3 KiB
TypeScript

import i18n from '@/lib/i18n';
export interface CodexTopLevelSettingsView {
model: string | null;
modelReasoningEffort: string | null;
modelContextWindow: number | null;
modelAutoCompactTokenLimit: number | null;
modelProvider: string | null;
approvalPolicy: string | null;
sandboxMode: string | null;
webSearch: string | null;
toolOutputTokenLimit: number | null;
personality: string | null;
}
export interface CodexProjectTrustEntry {
path: string;
trustLevel: string;
}
export interface CodexProfileEntry {
name: string;
values: CodexTopLevelSettingsView;
}
export interface CodexModelProviderEntry {
name: string;
displayName: string | null;
baseUrl: string | null;
envKey: string | null;
wireApi: string | null;
requiresOpenaiAuth: boolean;
supportsWebsockets: boolean;
}
export interface CodexMcpServerEntry {
name: string;
transport: 'stdio' | 'streamable-http';
command: string | null;
args: string[];
url: string | null;
enabled: boolean;
required: boolean;
startupTimeoutSec: number | null;
toolTimeoutSec: number | null;
enabledTools: string[];
disabledTools: string[];
isCcsManaged: boolean;
managementSurface: 'browser-settings' | null;
}
export interface CodexFeatureCatalogEntry {
name: string;
label: 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_FEATURE_NAMES = [
'multi_agent',
'unified_exec',
'shell_snapshot',
'apply_patch_freeform',
'js_repl',
'runtime_metrics',
'prevent_idle_sleep',
'fast_mode',
'apps',
'smart_approvals',
] as const;
export function getKnownCodexFeatures(): CodexFeatureCatalogEntry[] {
return [
{
name: 'multi_agent',
label: i18n.t('codex.featureMultiAgentLabel'),
description: i18n.t('codex.featureMultiAgentDesc'),
},
{
name: 'unified_exec',
label: i18n.t('codex.featureUnifiedExecLabel'),
description: i18n.t('codex.featureUnifiedExecDesc'),
},
{
name: 'shell_snapshot',
label: i18n.t('codex.featureShellSnapshotLabel'),
description: i18n.t('codex.featureShellSnapshotDesc'),
},
{
name: 'apply_patch_freeform',
label: i18n.t('codex.featureApplyPatchLabel'),
description: i18n.t('codex.featureApplyPatchDesc'),
},
{
name: 'js_repl',
label: i18n.t('codex.featureJsReplLabel'),
description: i18n.t('codex.featureJsReplDesc'),
},
{
name: 'runtime_metrics',
label: i18n.t('codex.featureRuntimeMetricsLabel'),
description: i18n.t('codex.featureRuntimeMetricsDesc'),
},
{
name: 'prevent_idle_sleep',
label: i18n.t('codex.featurePreventIdleSleepLabel'),
description: i18n.t('codex.featurePreventIdleSleepDesc'),
},
{
name: 'fast_mode',
label: i18n.t('codex.featureFastModeLabel'),
description: i18n.t('codex.featureFastModeDesc'),
},
{
name: 'apps',
label: i18n.t('codex.featureAppsLabel'),
description: i18n.t('codex.featureAppsDesc'),
},
{
name: 'smart_approvals',
label: i18n.t('codex.featureSmartApprovalsLabel'),
description: i18n.t('codex.featureSmartApprovalsDesc'),
},
];
}
function asObject(value: unknown): Record<string, unknown> | null {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function asString(value: unknown): string | null {
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
}
function asNumber(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
function asStringArray(value: unknown): string[] {
return Array.isArray(value)
? value
.filter((entry): entry is string => typeof entry === 'string')
.map((entry) => entry.trim())
: [];
}
export function readCodexTopLevelSettings(
config: Record<string, unknown> | null
): CodexTopLevelSettingsView {
return {
model: asString(config?.model),
modelReasoningEffort: asString(config?.model_reasoning_effort),
modelContextWindow: asNumber(config?.model_context_window),
modelAutoCompactTokenLimit: asNumber(config?.model_auto_compact_token_limit),
modelProvider: asString(config?.model_provider),
approvalPolicy: asString(config?.approval_policy),
sandboxMode: asString(config?.sandbox_mode),
webSearch: asString(config?.web_search),
toolOutputTokenLimit: asNumber(config?.tool_output_token_limit),
personality: asString(config?.personality),
};
}
export function readCodexProjectTrust(
config: Record<string, unknown> | null
): CodexProjectTrustEntry[] {
const projects = asObject(config?.projects);
if (!projects) return [];
return Object.entries(projects)
.map(([projectPath, value]) => {
const trustLevel = asString(asObject(value)?.trust_level);
return trustLevel ? { path: projectPath, trustLevel } : null;
})
.filter((entry): entry is CodexProjectTrustEntry => entry !== null)
.sort((left, right) => left.path.localeCompare(right.path));
}
export function readCodexProfiles(config: Record<string, unknown> | null): CodexProfileEntry[] {
const profiles = asObject(config?.profiles);
if (!profiles) return [];
return Object.entries(profiles)
.map(([name, value]) => ({ name, values: readCodexTopLevelSettings(asObject(value)) }))
.sort((left, right) => left.name.localeCompare(right.name));
}
export function readCodexModelProviders(
config: Record<string, unknown> | null
): CodexModelProviderEntry[] {
const providers = asObject(config?.model_providers);
if (!providers) return [];
return Object.entries(providers)
.map(([name, value]) => {
const provider = asObject(value);
if (!provider) return null;
return {
name,
displayName: asString(provider.name),
baseUrl: asString(provider.base_url),
envKey: asString(provider.env_key),
wireApi: asString(provider.wire_api),
requiresOpenaiAuth: provider.requires_openai_auth === true,
supportsWebsockets: provider.supports_websockets === true,
};
})
.filter((entry): entry is CodexModelProviderEntry => entry !== null)
.sort((left, right) => left.name.localeCompare(right.name));
}
export function readCodexMcpServers(config: Record<string, unknown> | null): CodexMcpServerEntry[] {
const servers = asObject(config?.mcp_servers);
if (!servers) return [];
return Object.entries(servers)
.map(([name, value]) => {
const server = asObject(value);
if (!server) return null;
const transport = asString(server.command) ? 'stdio' : 'streamable-http';
const startupTimeoutMs = asNumber(server.startup_timeout_ms);
return {
name,
transport,
command: asString(server.command),
args: asStringArray(server.args),
url: asString(server.url),
enabled: server.enabled !== false,
required: server.required === true,
startupTimeoutSec:
asNumber(server.startup_timeout_sec) ??
(startupTimeoutMs !== null ? startupTimeoutMs / 1000 : null),
toolTimeoutSec: asNumber(server.tool_timeout_sec),
enabledTools: asStringArray(server.enabled_tools),
disabledTools: asStringArray(server.disabled_tools),
isCcsManaged: name === 'ccs_browser',
managementSurface: name === 'ccs_browser' ? 'browser-settings' : null,
};
})
.filter((entry): entry is CodexMcpServerEntry => entry !== null)
.sort((left, right) => left.name.localeCompare(right.name));
}
export function readCodexFeatureState(
config: Record<string, unknown> | null
): Record<string, boolean | null> {
const features = asObject(config?.features);
const state: Record<string, boolean | null> = {};
for (const featureName of KNOWN_CODEX_FEATURE_NAMES) {
const value = features?.[featureName];
state[featureName] = typeof value === 'boolean' ? value : null;
}
if (features) {
for (const [name, value] of Object.entries(features)) {
if (!(name in state)) {
state[name] = typeof value === 'boolean' ? value : null;
}
}
}
return state;
}