feat(codex): add dashboard control center

- add guided editors for top-level settings, trust, profiles, providers, MCP, and features

- refresh raw snapshots after patch saves to avoid stale mtime conflicts

- block structured saves while raw TOML is dirty and add route plus hook coverage
This commit is contained in:
Tam Nhu Tran committed 2026-03-29 13:14:15 -04:00
1 parent 3c52b1ab6d
commit b47aa0d28d
18 files changed
+2959 -57

No files matched your search

@@ -0,0 +1,42 @@
import type { ReactNode } from 'react';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
interface CodexConfigCardShellProps {
title: string;
icon?: ReactNode;
badge?: string;
description?: string;
disabledReason?: string | null;
children: ReactNode;
}
export function CodexConfigCardShell({
title,
icon,
badge,
description,
disabledReason,
children,
}: CodexConfigCardShellProps) {
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base">
{icon}
{title}
{badge ? (
<Badge variant="outline" className="text-[10px] font-normal">
{badge}
</Badge>
) : null}
</CardTitle>
{description ? <p className="text-xs text-muted-foreground">{description}</p> : null}
</CardHeader>
<CardContent className="space-y-3">
{disabledReason ? <p className="text-xs text-amber-600">{disabledReason}</p> : null}
{children}
</CardContent>
</Card>
);
}
@@ -0,0 +1,129 @@
import { Sparkles } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge';
import type { CodexFeatureCatalogEntry } from '@/lib/codex-config';
import { CodexConfigCardShell } from './codex-config-card-shell';
interface CodexFeaturesCardProps {
catalog: CodexFeatureCatalogEntry[];
state: Record<string, boolean | null>;
disabled?: boolean;
disabledReason?: string | null;
onToggle: (feature: string, enabled: boolean | null) => Promise<void> | void;
}
export function CodexFeaturesCard({
catalog,
state,
disabled = false,
disabledReason,
onToggle,
}: CodexFeaturesCardProps) {
const knownFeatureNames = new Set(catalog.map((feature) => feature.name));
const configOnlyFeatures = Object.entries(state)
.filter(([name]) => !knownFeatureNames.has(name))
.sort(([left], [right]) => left.localeCompare(right));
return (
<CodexConfigCardShell
title="Features"
badge="features"
icon={<Sparkles className="h-4 w-4" />}
description="Toggle the supported Codex feature flags CCS can safely manage."
disabledReason={disabledReason}
>
<div className="space-y-2">
{catalog.map((feature) => {
const current = state[feature.name] ?? null;
return (
<div
key={feature.name}
className="flex items-center justify-between gap-3 rounded-md border px-3 py-2"
>
<div className="min-w-0">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">{feature.label}</p>
<Badge variant="outline" className="font-mono text-[10px]">
{feature.name}
</Badge>
</div>
<p className="text-xs text-muted-foreground">{feature.description}</p>
</div>
<div className="flex items-center gap-2">
{current !== null ? (
<Button
variant="outline"
size="sm"
onClick={() => onToggle(feature.name, null)}
disabled={disabled}
>
Use default
</Button>
) : null}
<Switch
checked={current === true}
onCheckedChange={(next) => onToggle(feature.name, next)}
disabled={disabled}
/>
</div>
</div>
);
})}
</div>
{configOnlyFeatures.length > 0 ? (
<div className="space-y-2">
<div className="space-y-1">
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
Existing config-only flags
</p>
<p className="text-xs text-muted-foreground">
These feature keys already exist in your `config.toml`, so CCS can surface them
without claiming full catalog coverage.
</p>
</div>
{configOnlyFeatures.map(([name, current]) => (
<div
key={name}
className="flex items-center justify-between gap-3 rounded-md border border-dashed px-3 py-2"
>
<div className="min-w-0">
<div className="flex items-center gap-2">
<p className="text-sm font-medium">{name}</p>
<Badge variant="secondary" className="text-[10px]">
existing
</Badge>
</div>
<p className="text-xs text-muted-foreground">
{current === null
? 'Stored in a non-boolean form. Use raw TOML if you need to edit it.'
: "Discovered from the current file instead of CCS's built-in catalog."}
</p>
</div>
{current === null ? (
<Badge variant="outline">Raw only</Badge>
) : (
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => onToggle(name, null)}
disabled={disabled}
>
Use default
</Button>
<Switch
checked={current === true}
onCheckedChange={(next) => onToggle(name, next)}
disabled={disabled}
/>
</div>
)}
</div>
))}
</div>
) : null}
</CodexConfigCardShell>
);
}
@@ -0,0 +1,278 @@
import { useMemo, useState } from 'react';
import { Loader2, PlugZap, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import type { CodexMcpServerPatchValues } from '@/hooks/use-codex-types';
import type { CodexMcpServerEntry } from '@/lib/codex-config';
import { CodexConfigCardShell } from './codex-config-card-shell';
interface CodexMcpServersCardProps {
entries: CodexMcpServerEntry[];
disabled?: boolean;
disabledReason?: string | null;
saving?: boolean;
onSave: (name: string, values: CodexMcpServerPatchValues) => Promise<void> | void;
onDelete: (name: string) => Promise<void> | void;
}
const EMPTY_MCP_SERVER_DRAFT: CodexMcpServerEntry = {
name: '',
transport: 'stdio',
command: null,
args: [],
url: null,
enabled: true,
required: false,
startupTimeoutSec: null,
toolTimeoutSec: null,
enabledTools: [],
disabledTools: [],
};
function toCsv(value: string[]) {
return value.join(', ');
}
function fromCsv(value: string) {
return value
.split(',')
.map((entry) => entry.trim())
.filter(Boolean);
}
interface McpServerEditorProps {
initialDraft: CodexMcpServerEntry;
isNew: boolean;
disabled: boolean;
saving: boolean;
canDelete: boolean;
onSave: (name: string, values: CodexMcpServerPatchValues) => Promise<void> | void;
onDelete: () => Promise<void> | void;
}
function McpServerEditor({
initialDraft,
isNew,
disabled,
saving,
canDelete,
onSave,
onDelete,
}: McpServerEditorProps) {
const [draft, setDraft] = useState<CodexMcpServerEntry>(initialDraft);
return (
<>
<div className="grid gap-3 sm:grid-cols-2">
<Input
value={draft.name}
onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))}
placeholder="playwright"
disabled={disabled || !isNew}
/>
<Select
value={draft.transport}
onValueChange={(next) =>
setDraft((current) => ({
...current,
transport: next as CodexMcpServerEntry['transport'],
}))
}
disabled={disabled}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="stdio">stdio</SelectItem>
<SelectItem value="streamable-http">streamable-http</SelectItem>
</SelectContent>
</Select>
{draft.transport === 'stdio' ? (
<>
<Input
value={draft.command ?? ''}
onChange={(event) =>
setDraft((current) => ({ ...current, command: event.target.value || null }))
}
placeholder="npx"
disabled={disabled}
/>
<Input
value={toCsv(draft.args)}
onChange={(event) =>
setDraft((current) => ({ ...current, args: fromCsv(event.target.value) }))
}
placeholder="@playwright/mcp@latest, --flag"
disabled={disabled}
/>
</>
) : (
<Input
className="sm:col-span-2"
value={draft.url ?? ''}
onChange={(event) =>
setDraft((current) => ({ ...current, url: event.target.value || null }))
}
placeholder="https://example.test/mcp"
disabled={disabled}
/>
)}
<Input
type="number"
min={1}
value={draft.startupTimeoutSec ?? ''}
onChange={(event) =>
setDraft((current) => ({
...current,
startupTimeoutSec: event.target.value ? Number(event.target.value) : null,
}))
}
placeholder="Startup timeout (sec)"
disabled={disabled}
/>
<Input
type="number"
min={1}
value={draft.toolTimeoutSec ?? ''}
onChange={(event) =>
setDraft((current) => ({
...current,
toolTimeoutSec: event.target.value ? Number(event.target.value) : null,
}))
}
placeholder="Tool timeout (sec)"
disabled={disabled}
/>
<Input
value={toCsv(draft.enabledTools)}
onChange={(event) =>
setDraft((current) => ({ ...current, enabledTools: fromCsv(event.target.value) }))
}
placeholder="enabled_tools"
disabled={disabled}
/>
<Input
value={toCsv(draft.disabledTools)}
onChange={(event) =>
setDraft((current) => ({ ...current, disabledTools: fromCsv(event.target.value) }))
}
placeholder="disabled_tools"
disabled={disabled}
/>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<label className="flex items-center justify-between rounded-md border px-3 py-2 text-sm">
Enabled
<Switch
checked={draft.enabled}
onCheckedChange={(next) => setDraft((current) => ({ ...current, enabled: next }))}
disabled={disabled}
/>
</label>
<label className="flex items-center justify-between rounded-md border px-3 py-2 text-sm">
Required
<Switch
checked={draft.required}
onCheckedChange={(next) => setDraft((current) => ({ ...current, required: next }))}
disabled={disabled}
/>
</label>
</div>
<div className="flex justify-between gap-2">
<Button variant="outline" onClick={onDelete} disabled={disabled || saving || !canDelete}>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</Button>
<Button
onClick={() =>
onSave(draft.name, {
transport: draft.transport,
command: draft.command,
args: draft.args,
url: draft.url,
enabled: draft.enabled,
required: draft.required,
startupTimeoutSec: draft.startupTimeoutSec,
toolTimeoutSec: draft.toolTimeoutSec,
enabledTools: draft.enabledTools,
disabledTools: draft.disabledTools,
})
}
disabled={disabled || saving || draft.name.trim().length === 0}
>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Save MCP server
</Button>
</div>
</>
);
}
export function CodexMcpServersCard({
entries,
disabled = false,
disabledReason,
saving = false,
onSave,
onDelete,
}: CodexMcpServersCardProps) {
const [selectedName, setSelectedName] = useState('new');
const selectedEntry = useMemo(
() => entries.find((entry) => entry.name === selectedName) ?? null,
[entries, selectedName]
);
const draftSeed = selectedEntry ?? EMPTY_MCP_SERVER_DRAFT;
const draftKey = JSON.stringify(draftSeed);
return (
<CodexConfigCardShell
title="MCP servers"
badge="mcp_servers"
icon={<PlugZap className="h-4 w-4" />}
description="Manage the safe MCP transport fields. Keep auth headers and bearer tokens in raw TOML."
disabledReason={disabledReason}
>
<Select value={selectedName} onValueChange={setSelectedName} disabled={disabled}>
<SelectTrigger>
<SelectValue placeholder="Select MCP server" />
</SelectTrigger>
<SelectContent>
<SelectItem value="new">Create new MCP server</SelectItem>
{entries.map((entry) => (
<SelectItem key={entry.name} value={entry.name}>
{entry.name}
</SelectItem>
))}
</SelectContent>
</Select>
<McpServerEditor
key={draftKey}
initialDraft={draftSeed}
isNew={selectedName === 'new'}
disabled={disabled}
saving={saving}
canDelete={selectedEntry !== null}
onDelete={async () => {
if (!selectedEntry) return;
await onDelete(selectedEntry.name);
setSelectedName('new');
}}
onSave={async (name, values) => {
await onSave(name, values);
setSelectedName(name);
}}
/>
</CodexConfigCardShell>
);
}
@@ -0,0 +1,209 @@
import { useMemo, useState } from 'react';
import { KeyRound, Loader2, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import type { CodexModelProviderPatchValues } from '@/hooks/use-codex-types';
import type { CodexModelProviderEntry } from '@/lib/codex-config';
import { CodexConfigCardShell } from './codex-config-card-shell';
interface CodexModelProvidersCardProps {
entries: CodexModelProviderEntry[];
disabled?: boolean;
disabledReason?: string | null;
saving?: boolean;
onSave: (name: string, values: CodexModelProviderPatchValues) => Promise<void> | void;
onDelete: (name: string) => Promise<void> | void;
}
const EMPTY_MODEL_PROVIDER_DRAFT: CodexModelProviderEntry = {
name: '',
displayName: null,
baseUrl: null,
envKey: null,
wireApi: 'responses',
requiresOpenaiAuth: false,
supportsWebsockets: false,
};
interface ModelProviderEditorProps {
initialDraft: CodexModelProviderEntry;
isNew: boolean;
disabled: boolean;
saving: boolean;
canDelete: boolean;
onSave: (name: string, values: CodexModelProviderPatchValues) => Promise<void> | void;
onDelete: () => Promise<void> | void;
}
function ModelProviderEditor({
initialDraft,
isNew,
disabled,
saving,
canDelete,
onSave,
onDelete,
}: ModelProviderEditorProps) {
const [draft, setDraft] = useState<CodexModelProviderEntry>(initialDraft);
return (
<>
<div className="grid gap-3 sm:grid-cols-2">
<Input
value={draft.name}
onChange={(event) => setDraft((current) => ({ ...current, name: event.target.value }))}
placeholder="Provider id"
disabled={disabled || !isNew}
/>
<Input
value={draft.displayName ?? ''}
onChange={(event) =>
setDraft((current) => ({ ...current, displayName: event.target.value || null }))
}
placeholder="Display name"
disabled={disabled}
/>
<Input
value={draft.baseUrl ?? ''}
onChange={(event) =>
setDraft((current) => ({ ...current, baseUrl: event.target.value || null }))
}
placeholder="http://127.0.0.1:8317/api/provider/codex"
disabled={disabled}
/>
<Input
value={draft.envKey ?? ''}
onChange={(event) =>
setDraft((current) => ({ ...current, envKey: event.target.value || null }))
}
placeholder="CLIPROXY_API_KEY"
disabled={disabled}
/>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<Select
value={draft.wireApi ?? 'responses'}
onValueChange={(next) => setDraft((current) => ({ ...current, wireApi: next }))}
disabled={disabled}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="responses">responses</SelectItem>
</SelectContent>
</Select>
<label className="flex items-center justify-between rounded-md border px-3 py-2 text-sm">
Requires OpenAI auth
<Switch
checked={draft.requiresOpenaiAuth}
onCheckedChange={(next) =>
setDraft((current) => ({ ...current, requiresOpenaiAuth: next }))
}
disabled={disabled}
/>
</label>
<label className="flex items-center justify-between rounded-md border px-3 py-2 text-sm">
Supports websockets
<Switch
checked={draft.supportsWebsockets}
onCheckedChange={(next) =>
setDraft((current) => ({ ...current, supportsWebsockets: next }))
}
disabled={disabled}
/>
</label>
</div>
<div className="flex justify-between gap-2">
<Button variant="outline" onClick={onDelete} disabled={disabled || saving || !canDelete}>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</Button>
<Button
onClick={() =>
onSave(draft.name, {
displayName: draft.displayName,
baseUrl: draft.baseUrl,
envKey: draft.envKey,
wireApi: draft.wireApi,
requiresOpenaiAuth: draft.requiresOpenaiAuth,
supportsWebsockets: draft.supportsWebsockets,
})
}
disabled={disabled || saving || draft.name.trim().length === 0}
>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Save provider
</Button>
</div>
</>
);
}
export function CodexModelProvidersCard({
entries,
disabled = false,
disabledReason,
saving = false,
onSave,
onDelete,
}: CodexModelProvidersCardProps) {
const [selectedName, setSelectedName] = useState<string>('new');
const selectedEntry = useMemo(
() => entries.find((entry) => entry.name === selectedName) ?? null,
[entries, selectedName]
);
const draftSeed = selectedEntry ?? EMPTY_MODEL_PROVIDER_DRAFT;
const draftKey = JSON.stringify(draftSeed);
return (
<CodexConfigCardShell
title="Model providers"
badge="model_providers"
icon={<KeyRound className="h-4 w-4" />}
description="Edit the common provider fields CCS can support safely. Keep secret migration and inline bearer tokens in raw TOML."
disabledReason={disabledReason}
>
<Select value={selectedName} onValueChange={setSelectedName} disabled={disabled}>
<SelectTrigger>
<SelectValue placeholder="Select provider" />
</SelectTrigger>
<SelectContent>
<SelectItem value="new">Create new provider</SelectItem>
{entries.map((entry) => (
<SelectItem key={entry.name} value={entry.name}>
{entry.name}
</SelectItem>
))}
</SelectContent>
</Select>
<ModelProviderEditor
key={draftKey}
initialDraft={draftSeed}
isNew={selectedName === 'new'}
disabled={disabled}
saving={saving}
canDelete={selectedEntry !== null}
onDelete={async () => {
if (!selectedEntry) return;
await onDelete(selectedEntry.name);
setSelectedName('new');
}}
onSave={async (name, values) => {
await onSave(name, values);
setSelectedName(name);
}}
/>
</CodexConfigCardShell>
);
}
@@ -0,0 +1,250 @@
import { useMemo, useState } from 'react';
import { Layers3, Loader2, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import type { CodexProfilePatchValues } from '@/hooks/use-codex-types';
import type { CodexProfileEntry } from '@/lib/codex-config';
import { CodexConfigCardShell } from './codex-config-card-shell';
interface CodexProfilesCardProps {
activeProfile: string | null;
entries: CodexProfileEntry[];
providerNames: string[];
disabled?: boolean;
disabledReason?: string | null;
saving?: boolean;
onSave: (
name: string,
values: CodexProfilePatchValues,
setAsActive: boolean
) => Promise<void> | void;
onDelete: (name: string) => Promise<void> | void;
onSetActive: (name: string) => Promise<void> | void;
}
interface ProfileEditorProps {
initialName: string;
initialModel: string | null;
initialProvider: string | null;
initialEffort: string | null;
providerNames: string[];
activeProfile: string | null;
selectedEntryName: string | null;
disabled: boolean;
saving: boolean;
onSave: (
name: string,
values: CodexProfilePatchValues,
setAsActive: boolean
) => Promise<void> | void;
onDelete: () => Promise<void> | void;
onSetActive: () => Promise<void> | void;
}
function ProfileEditor({
initialName,
initialModel,
initialProvider,
initialEffort,
providerNames,
activeProfile,
selectedEntryName,
disabled,
saving,
onSave,
onDelete,
onSetActive,
}: ProfileEditorProps) {
const [nameDraft, setNameDraft] = useState(initialName);
const [modelDraft, setModelDraft] = useState<string | null>(initialModel);
const [providerDraft, setProviderDraft] = useState<string | null>(initialProvider);
const [effortDraft, setEffortDraft] = useState<string | null>(initialEffort);
return (
<>
<div className="grid gap-3 sm:grid-cols-2">
<Input
value={nameDraft}
onChange={(event) => setNameDraft(event.target.value)}
placeholder="deep-review"
disabled={disabled || selectedEntryName !== null}
/>
<Input
value={modelDraft ?? ''}
onChange={(event) => setModelDraft(event.target.value || null)}
placeholder="gpt-5.4"
disabled={disabled}
/>
<Select
value={providerDraft ?? '__unset__'}
onValueChange={(next) => setProviderDraft(next === '__unset__' ? null : next)}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder="Use global provider" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__unset__">Use global provider</SelectItem>
{providerNames.map((name) => (
<SelectItem key={name} value={name}>
{name}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={effortDraft ?? '__unset__'}
onValueChange={(next) => setEffortDraft(next === '__unset__' ? null : next)}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder="Use global effort" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__unset__">Use global effort</SelectItem>
{['minimal', 'low', 'medium', 'high', 'xhigh'].map((value) => (
<SelectItem key={value} value={value}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex justify-between gap-2">
<div className="flex gap-2">
<Button
variant="outline"
onClick={onDelete}
disabled={disabled || saving || !selectedEntryName}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
</Button>
<Button
variant="outline"
onClick={onSetActive}
disabled={
disabled || saving || !selectedEntryName || selectedEntryName === activeProfile
}
>
Set active
</Button>
</div>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() =>
onSave(
nameDraft,
{
model: modelDraft,
modelProvider: providerDraft,
modelReasoningEffort: effortDraft,
},
false
)
}
disabled={disabled || saving || nameDraft.trim().length === 0}
>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Save profile
</Button>
<Button
onClick={() =>
onSave(
nameDraft,
{
model: modelDraft,
modelProvider: providerDraft,
modelReasoningEffort: effortDraft,
},
true
)
}
disabled={disabled || saving || nameDraft.trim().length === 0}
>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Save + activate
</Button>
</div>
</div>
</>
);
}
export function CodexProfilesCard({
activeProfile,
entries,
providerNames,
disabled = false,
disabledReason,
saving = false,
onSave,
onDelete,
onSetActive,
}: CodexProfilesCardProps) {
const [selectedName, setSelectedName] = useState('new');
const selectedEntry = useMemo(
() => entries.find((entry) => entry.name === selectedName) ?? null,
[entries, selectedName]
);
const draftKey = JSON.stringify(selectedEntry ?? { name: '', values: {} });
return (
<CodexConfigCardShell
title="Profiles"
badge="profiles"
icon={<Layers3 className="h-4 w-4" />}
description="Create reusable Codex overlays and set the active default profile."
disabledReason={disabledReason}
>
<Select value={selectedName} onValueChange={setSelectedName} disabled={disabled}>
<SelectTrigger>
<SelectValue placeholder="Select profile" />
</SelectTrigger>
<SelectContent>
<SelectItem value="new">Create new profile</SelectItem>
{entries.map((entry) => (
<SelectItem key={entry.name} value={entry.name}>
{entry.name}
{entry.name === activeProfile ? ' (active)' : ''}
</SelectItem>
))}
</SelectContent>
</Select>
<ProfileEditor
key={draftKey}
initialName={selectedEntry?.name ?? ''}
initialModel={selectedEntry?.values.model ?? null}
initialProvider={selectedEntry?.values.modelProvider ?? null}
initialEffort={selectedEntry?.values.modelReasoningEffort ?? null}
providerNames={providerNames}
activeProfile={activeProfile}
selectedEntryName={selectedEntry?.name ?? null}
disabled={disabled}
saving={saving}
onDelete={async () => {
if (!selectedEntry) return;
await onDelete(selectedEntry.name);
setSelectedName('new');
}}
onSetActive={async () => {
if (!selectedEntry) return;
await onSetActive(selectedEntry.name);
}}
onSave={async (name, values, setAsActive) => {
await onSave(name, values, setAsActive);
setSelectedName(name);
}}
/>
</CodexConfigCardShell>
);
}
@@ -0,0 +1,141 @@
import { useState } from 'react';
import { FolderCheck, Loader2, Trash2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import type { CodexProjectTrustEntry } from '@/lib/codex-config';
import { CodexConfigCardShell } from './codex-config-card-shell';
interface CodexProjectTrustCardProps {
workspacePath: string;
entries: CodexProjectTrustEntry[];
disabled?: boolean;
disabledReason?: string | null;
saving?: boolean;
onSave: (path: string, trustLevel: string | null) => Promise<void> | void;
}
interface ProjectTrustComposerProps {
workspacePath: string;
disabled: boolean;
saving: boolean;
onSave: (path: string, trustLevel: string | null) => Promise<void> | void;
}
function ProjectTrustComposer({
workspacePath,
disabled,
saving,
onSave,
}: ProjectTrustComposerProps) {
const [pathDraft, setPathDraft] = useState(workspacePath);
const [trustLevel, setTrustLevel] = useState('trusted');
return (
<div className="grid gap-2 sm:grid-cols-[1fr_160px_auto]">
<Input
value={pathDraft}
onChange={(event) => setPathDraft(event.target.value)}
placeholder="~/repo or /absolute/path"
disabled={disabled}
/>
<Select value={trustLevel} onValueChange={setTrustLevel} disabled={disabled}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="trusted">trusted</SelectItem>
<SelectItem value="ask">ask</SelectItem>
</SelectContent>
</Select>
<Button onClick={() => onSave(pathDraft, trustLevel)} disabled={disabled || saving}>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Save trust
</Button>
</div>
);
}
export function CodexProjectTrustCard({
workspacePath,
entries,
disabled = false,
disabledReason,
saving = false,
onSave,
}: CodexProjectTrustCardProps) {
return (
<CodexConfigCardShell
title="Project trust"
badge="projects"
icon={<FolderCheck className="h-4 w-4" />}
description="Trust current workspaces or remove stale trust entries without opening raw TOML."
disabledReason={disabledReason}
>
<p className="text-xs text-muted-foreground">
Paths must be absolute or start with <code>~/</code>. Relative paths are rejected so CCS
does not trust the wrong folder.
</p>
<ProjectTrustComposer
key={workspacePath}
workspacePath={workspacePath}
disabled={disabled}
saving={saving}
onSave={onSave}
/>
<Button
variant="outline"
className="w-full justify-start"
onClick={() => onSave(workspacePath, 'trusted')}
disabled={disabled || saving}
>
Trust current workspace
</Button>
<div className="space-y-2">
{entries.length === 0 ? (
<p className="text-xs text-muted-foreground">No explicit project trust entries saved.</p>
) : (
entries.map((entry) => (
<div
key={entry.path}
className="flex items-center justify-between gap-2 rounded-md border px-3 py-2"
>
<div className="min-w-0">
<p className="truncate text-sm font-medium">{entry.path}</p>
<p className="text-xs text-muted-foreground">trust_level = {entry.trustLevel}</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() =>
onSave(entry.path, entry.trustLevel === 'trusted' ? 'ask' : 'trusted')
}
disabled={disabled || saving}
>
Toggle
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => onSave(entry.path, null)}
disabled={disabled || saving}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
))
)}
</div>
</CodexConfigCardShell>
);
}
@@ -0,0 +1,280 @@
import { useState } from 'react';
import { Loader2, SlidersHorizontal } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import type { CodexTopLevelSettingsPatch } from '@/hooks/use-codex-types';
import type { CodexTopLevelSettingsView } from '@/lib/codex-config';
import { CodexConfigCardShell } from './codex-config-card-shell';
const UNSET = '__unset__';
interface CodexTopLevelControlsCardProps {
values: CodexTopLevelSettingsView;
providerNames: string[];
disabled?: boolean;
disabledReason?: string | null;
saving?: boolean;
onSave: (values: CodexTopLevelSettingsPatch) => Promise<void> | void;
}
function toSelectValue(value: string | null | undefined) {
return value ?? UNSET;
}
function withCurrentValue(options: string[], current: string | null | undefined) {
return current && !options.includes(current) ? [current, ...options] : options;
}
interface TopLevelControlsFormProps {
initialValues: CodexTopLevelSettingsView;
providerNames: string[];
disabled: boolean;
saving: boolean;
onSave: (values: CodexTopLevelSettingsPatch) => Promise<void> | void;
}
function TopLevelControlsForm({
initialValues,
providerNames,
disabled,
saving,
onSave,
}: TopLevelControlsFormProps) {
const [draft, setDraft] = useState<CodexTopLevelSettingsView>(initialValues);
const reasoningOptions = withCurrentValue(
['minimal', 'low', 'medium', 'high', 'xhigh'],
draft.modelReasoningEffort
);
const providerOptions = withCurrentValue(providerNames, draft.modelProvider);
const approvalOptions = withCurrentValue(
['on-request', 'never', 'untrusted'],
draft.approvalPolicy
);
const sandboxOptions = withCurrentValue(
['read-only', 'workspace-write', 'danger-full-access'],
draft.sandboxMode
);
const webSearchOptions = withCurrentValue(['cached', 'live', 'disabled'], draft.webSearch);
const personalityOptions = withCurrentValue(
['default', 'pragmatic', 'concise', 'direct'],
draft.personality
);
return (
<>
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1">
<p className="text-xs font-medium">Model</p>
<Input
value={draft.model ?? ''}
onChange={(event) =>
setDraft((current) => ({ ...current, model: event.target.value || null }))
}
placeholder="gpt-5.4"
disabled={disabled}
/>
</div>
<div className="space-y-1">
<p className="text-xs font-medium">Reasoning effort</p>
<Select
value={toSelectValue(draft.modelReasoningEffort)}
onValueChange={(next) =>
setDraft((current) => ({
...current,
modelReasoningEffort: next === UNSET ? null : next,
}))
}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder="Use default" />
</SelectTrigger>
<SelectContent>
<SelectItem value={UNSET}>Use default</SelectItem>
{reasoningOptions.map((value) => (
<SelectItem key={value} value={value}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<p className="text-xs font-medium">Default provider</p>
<Select
value={toSelectValue(draft.modelProvider)}
onValueChange={(next) =>
setDraft((current) => ({ ...current, modelProvider: next === UNSET ? null : next }))
}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder="Use Codex default" />
</SelectTrigger>
<SelectContent>
<SelectItem value={UNSET}>Use Codex default</SelectItem>
{providerOptions.map((name) => (
<SelectItem key={name} value={name}>
{name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<p className="text-xs font-medium">Approval policy</p>
<Select
value={toSelectValue(draft.approvalPolicy)}
onValueChange={(next) =>
setDraft((current) => ({ ...current, approvalPolicy: next === UNSET ? null : next }))
}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder="Use default" />
</SelectTrigger>
<SelectContent>
<SelectItem value={UNSET}>Use default</SelectItem>
{approvalOptions.map((value) => (
<SelectItem key={value} value={value}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<p className="text-xs font-medium">Sandbox mode</p>
<Select
value={toSelectValue(draft.sandboxMode)}
onValueChange={(next) =>
setDraft((current) => ({ ...current, sandboxMode: next === UNSET ? null : next }))
}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder="Use default" />
</SelectTrigger>
<SelectContent>
<SelectItem value={UNSET}>Use default</SelectItem>
{sandboxOptions.map((value) => (
<SelectItem key={value} value={value}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<p className="text-xs font-medium">Web search</p>
<Select
value={toSelectValue(draft.webSearch)}
onValueChange={(next) =>
setDraft((current) => ({ ...current, webSearch: next === UNSET ? null : next }))
}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder="Use default" />
</SelectTrigger>
<SelectContent>
<SelectItem value={UNSET}>Use default</SelectItem>
{webSearchOptions.map((value) => (
<SelectItem key={value} value={value}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<p className="text-xs font-medium">Tool output token limit</p>
<Input
type="number"
min={1}
value={draft.toolOutputTokenLimit ?? ''}
onChange={(event) =>
setDraft((current) => ({
...current,
toolOutputTokenLimit: event.target.value ? Number(event.target.value) : null,
}))
}
placeholder="25000"
disabled={disabled}
/>
</div>
<div className="space-y-1">
<p className="text-xs font-medium">Personality</p>
<Select
value={toSelectValue(draft.personality)}
onValueChange={(next) =>
setDraft((current) => ({ ...current, personality: next === UNSET ? null : next }))
}
disabled={disabled}
>
<SelectTrigger>
<SelectValue placeholder="Use default" />
</SelectTrigger>
<SelectContent>
<SelectItem value={UNSET}>Use default</SelectItem>
{personalityOptions.map((value) => (
<SelectItem key={value} value={value}>
{value}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="flex justify-end">
<Button onClick={() => onSave(draft)} disabled={disabled || saving}>
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
Save top-level settings
</Button>
</div>
</>
);
}
export function CodexTopLevelControlsCard({
values,
providerNames,
disabled = false,
disabledReason,
saving = false,
onSave,
}: CodexTopLevelControlsCardProps) {
return (
<CodexConfigCardShell
title="Top-level controls"
badge="config.toml"
icon={<SlidersHorizontal className="h-4 w-4" />}
description="Structured controls for the stable top-level Codex settings users touch most often."
disabledReason={disabledReason}
>
<TopLevelControlsForm
key={JSON.stringify(values)}
initialValues={values}
providerNames={providerNames}
disabled={disabled}
saving={saving}
onSave={onSave}
/>
</CodexConfigCardShell>
);
}
+84
View File
@@ -88,11 +88,14 @@ export interface CodexSupportMatrixEntry {
export interface CodexUserConfigDiagnostics {
model: string | null;
modelReasoningEffort: string | null;
modelProvider: string | null;
activeProfile: string | null;
approvalPolicy: string | null;
sandboxMode: string | null;
webSearch: string | null;
toolOutputTokenLimit: number | null;
personality: string | null;
topLevelKeys: string[];
profileCount: number;
profileNames: string[];
@@ -111,6 +114,7 @@ export interface CodexUserConfigDiagnostics {
export interface CodexDashboardDiagnostics {
binary: CodexBinaryDiagnostics;
file: CodexConfigFileDiagnostics;
workspacePath: string;
config: CodexUserConfigDiagnostics;
supportMatrix: CodexSupportMatrixEntry[];
warnings: string[];
@@ -126,3 +130,83 @@ export interface CodexRawConfigResponse {
config: Record<string, unknown> | null;
parseError: string | null;
}
export interface CodexTopLevelSettingsPatch {
model?: string | null;
modelReasoningEffort?: string | null;
modelProvider?: string | null;
approvalPolicy?: string | null;
sandboxMode?: string | null;
webSearch?: string | null;
toolOutputTokenLimit?: number | null;
personality?: string | null;
}
export type CodexProfilePatchValues = CodexTopLevelSettingsPatch;
export interface CodexModelProviderPatchValues {
displayName?: string | null;
baseUrl?: string | null;
envKey?: string | null;
wireApi?: string | null;
requiresOpenaiAuth?: boolean | null;
supportsWebsockets?: boolean | null;
}
export interface CodexMcpServerPatchValues {
transport: 'stdio' | 'streamable-http';
command?: string | null;
args?: string[] | null;
url?: string | null;
enabled?: boolean | null;
required?: boolean | null;
startupTimeoutSec?: number | null;
toolTimeoutSec?: number | null;
enabledTools?: string[] | null;
disabledTools?: string[] | null;
}
export type CodexConfigPatchInput =
| {
kind: 'top-level';
expectedMtime?: number;
values: CodexTopLevelSettingsPatch;
}
| {
kind: 'project-trust';
expectedMtime?: number;
path: string;
trustLevel: string | null;
}
| {
kind: 'feature';
expectedMtime?: number;
feature: string;
enabled: boolean | null;
}
| {
kind: 'profile';
expectedMtime?: number;
action: 'set-active' | 'upsert' | 'delete';
name: string;
values?: CodexProfilePatchValues;
setAsActive?: boolean;
}
| {
kind: 'model-provider';
expectedMtime?: number;
action: 'upsert' | 'delete';
name: string;
values?: CodexModelProviderPatchValues;
}
| {
kind: 'mcp-server';
expectedMtime?: number;
action: 'upsert' | 'delete';
name: string;
values?: CodexMcpServerPatchValues;
};
export interface CodexConfigPatchResult extends CodexRawConfigResponse {
success: true;
}
+37 -1
View File
@@ -2,7 +2,12 @@ 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 './use-codex-types';
import type {
CodexConfigPatchInput,
CodexConfigPatchResult,
CodexDashboardDiagnostics,
CodexRawConfigResponse,
} from './use-codex-types';
type CodexRawConfig = CodexRawConfigResponse;
@@ -16,6 +21,8 @@ interface SaveCodexRawConfigResponse {
mtime: number;
}
type PatchCodexConfigResponse = CodexConfigPatchResult;
function parseCodexRawConfigText(rawText: string): {
config: Record<string, unknown> | null;
parseError: string | null;
@@ -69,6 +76,21 @@ async function saveCodexRawConfig(
return res.json();
}
async function patchCodexConfig(data: CodexConfigPatchInput): Promise<PatchCodexConfigResponse> {
const res = await fetch(withApiBase('/codex/config/patch'), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (res.status === 409) throw new ApiConflictError('Codex config changed externally');
if (!res.ok) {
const payload = (await res.json().catch(() => null)) as { error?: string } | null;
throw new Error(payload?.error || 'Failed to patch Codex config');
}
return res.json();
}
export function useCodex() {
const queryClient = useQueryClient();
@@ -105,6 +127,14 @@ export function useCodex() {
},
});
const patchConfigMutation = useMutation({
mutationFn: patchCodexConfig,
onSuccess: (result) => {
queryClient.setQueryData<CodexRawConfig>(['codex-raw-config'], result);
queryClient.invalidateQueries({ queryKey: ['codex-diagnostics'] });
},
});
return useMemo(
() => ({
diagnostics: diagnosticsQuery.data,
@@ -120,6 +150,9 @@ export function useCodex() {
saveRawConfig: saveRawConfigMutation.mutate,
saveRawConfigAsync: saveRawConfigMutation.mutateAsync,
isSavingRawConfig: saveRawConfigMutation.isPending,
patchConfig: patchConfigMutation.mutate,
patchConfigAsync: patchConfigMutation.mutateAsync,
isPatchingConfig: patchConfigMutation.isPending,
}),
[
diagnosticsQuery.data,
@@ -133,6 +166,9 @@ export function useCodex() {
saveRawConfigMutation.mutate,
saveRawConfigMutation.mutateAsync,
saveRawConfigMutation.isPending,
patchConfigMutation.mutate,
patchConfigMutation.mutateAsync,
patchConfigMutation.isPending,
]
);
}
+225
View File
@@ -0,0 +1,225 @@
export interface CodexTopLevelSettingsView {
model: string | null;
modelReasoningEffort: string | 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[];
}
export interface CodexFeatureCatalogEntry {
name: string;
label: string;
description: string;
}
export const KNOWN_CODEX_FEATURES: CodexFeatureCatalogEntry[] = [
{
name: 'multi_agent',
label: 'Multi-agent',
description: 'Enable subagent collaboration tools.',
},
{
name: 'unified_exec',
label: 'Unified exec',
description: 'Use the PTY-backed unified exec tool.',
},
{
name: 'shell_snapshot',
label: 'Shell snapshot',
description: 'Reuse shell environment snapshots.',
},
{
name: 'apply_patch_freeform',
label: 'Apply patch',
description: 'Enable freeform apply_patch edits.',
},
{ name: 'js_repl', label: 'JS REPL', description: 'Enable the Node-backed JavaScript REPL.' },
{
name: 'runtime_metrics',
label: 'Runtime metrics',
description: 'Collect Codex runtime metrics.',
},
{
name: 'prevent_idle_sleep',
label: 'Prevent idle sleep',
description: 'Keep the machine awake while active.',
},
{ name: 'fast_mode', label: 'Fast mode', description: 'Allow the fast service tier path.' },
{ name: 'apps', label: 'Apps', description: 'Enable ChatGPT Apps and connectors support.' },
{
name: 'smart_approvals',
label: 'Smart approvals',
description: 'Route eligible approvals through the guardian flow.',
},
];
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),
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';
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),
toolTimeoutSec: asNumber(server.tool_timeout_sec),
enabledTools: asStringArray(server.enabled_tools),
disabledTools: asStringArray(server.disabled_tools),
};
})
.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 feature of KNOWN_CODEX_FEATURES) {
const value = features?.[feature.name];
state[feature.name] = 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;
}
+220 -54
View File
@@ -1,4 +1,4 @@
import { type ReactNode, useState } from 'react';
import { type ReactNode, useMemo, useState } from 'react';
import { parse as parseToml } from 'smol-toml';
import { toast } from 'sonner';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
@@ -6,7 +6,6 @@ import {
AlertTriangle,
CheckCircle2,
ExternalLink,
FileWarning,
Folder,
GripVertical,
Info,
@@ -19,7 +18,13 @@ import {
import { useCodex } from '@/hooks/use-codex';
import { isApiConflictError } from '@/lib/api-client';
import { RawConfigEditorPanel } from '@/components/compatible-cli/raw-json-settings-editor-panel';
import { UsageCommand } from '@/components/cliproxy/provider-editor/usage-command';
import { CodexFeaturesCard } from '@/components/compatible-cli/codex-features-card';
import { CodexMcpServersCard } from '@/components/compatible-cli/codex-mcp-servers-card';
import { CodexModelProvidersCard } from '@/components/compatible-cli/codex-model-providers-card';
import { CodexProfilesCard } from '@/components/compatible-cli/codex-profiles-card';
import { CodexProjectTrustCard } from '@/components/compatible-cli/codex-project-trust-card';
import { CodexTopLevelControlsCard } from '@/components/compatible-cli/codex-top-level-controls-card';
import { QuickCommands } from '@/components/shared';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
@@ -33,6 +38,15 @@ import {
TableRow,
} from '@/components/ui/table';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
KNOWN_CODEX_FEATURES,
readCodexFeatureState,
readCodexMcpServers,
readCodexModelProviders,
readCodexProfiles,
readCodexProjectTrust,
readCodexTopLevelSettings,
} from '@/lib/codex-config';
import { cn } from '@/lib/utils';
const DEFAULT_CODEX_DOC_LINKS = [
@@ -157,9 +171,12 @@ export function CodexPage() {
refetchDiagnostics,
rawConfig,
rawConfigLoading,
rawConfigError,
refetchRawConfig,
saveRawConfigAsync,
isSavingRawConfig,
patchConfigAsync,
isPatchingConfig,
} = useCodex();
const [rawDraftText, setRawDraftText] = useState<string | null>(null);
@@ -170,6 +187,34 @@ export function CodexPage() {
const rawEditorValidation = rawEditorParsed.valid
? { valid: true as const }
: { valid: false as const, error: rawEditorParsed.error };
const controlsConfig = rawConfig?.config ?? null;
const structuredControlsDisabled =
rawConfigLoading || !rawConfig || rawConfigDirty || rawConfig?.parseError !== null;
const controlsDisabledReason = rawConfigError
? 'Structured controls unavailable: failed to load the current config.toml.'
: rawConfigDirty
? rawEditorValidation.valid
? 'Save or discard raw TOML edits before using structured controls.'
: 'Fix or discard raw TOML edits before using structured controls.'
: rawConfig?.parseError
? `Structured controls disabled: ${rawConfig.parseError}`
: null;
const topLevelSettings = useMemo(
() => readCodexTopLevelSettings(controlsConfig),
[controlsConfig]
);
const projectTrustEntries = useMemo(
() => readCodexProjectTrust(controlsConfig),
[controlsConfig]
);
const profileEntries = useMemo(() => readCodexProfiles(controlsConfig), [controlsConfig]);
const modelProviderEntries = useMemo(
() => readCodexModelProviders(controlsConfig),
[controlsConfig]
);
const mcpServerEntries = useMemo(() => readCodexMcpServers(controlsConfig), [controlsConfig]);
const featureState = useMemo(() => readCodexFeatureState(controlsConfig), [controlsConfig]);
const setRawEditorDraftText = (nextText: string) => {
if (nextText === rawBaseText) {
@@ -206,6 +251,26 @@ export function CodexPage() {
}
};
const runConfigPatch = async (
patch: Parameters<typeof patchConfigAsync>[0],
successMessage: string
) => {
try {
await patchConfigAsync({
...patch,
expectedMtime: rawConfig?.exists ? rawConfig.mtime : undefined,
});
setRawDraftText(null);
toast.success(successMessage);
} catch (error) {
if (isApiConflictError(error)) {
toast.error('config.toml changed externally. Refresh and retry.');
} else {
toast.error((error as Error).message || 'Failed to update Codex config.');
}
}
};
const renderOverview = () => {
if (diagnosticsLoading) {
return (
@@ -242,7 +307,7 @@ export function CodexPage() {
<div className="shrink-0 px-4 pt-4">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="routing">Runtime &amp; Routing</TabsTrigger>
<TabsTrigger value="controls">Control Center</TabsTrigger>
<TabsTrigger value="docs">Docs</TabsTrigger>
</TabsList>
</div>
@@ -424,30 +489,31 @@ export function CodexPage() {
</CardContent>
</Card>
{diagnostics.warnings.length > 0 && (
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base">
<AlertTriangle className="h-4 w-4 text-amber-600" />
Warnings
</CardTitle>
</CardHeader>
<CardContent className="space-y-1.5">
{diagnostics.warnings.map((warning) => (
<p key={warning} className="text-sm text-amber-800 dark:text-amber-300">
- {warning}
</p>
))}
</CardContent>
</Card>
)}
</div>
</ScrollArea>
</TabsContent>
<QuickCommands
snippets={[
{
label: 'Native Codex',
command: 'ccs-codex',
description: 'Launch the native Codex runtime alias.',
},
{
label: 'Short alias',
command: 'ccsx',
description: 'Launch the short Codex runtime alias.',
},
{
label: 'Target override',
command: 'ccs codex --target codex "your prompt"',
description: 'Run a CCS profile on the native Codex target.',
},
{
label: 'Workspace trust',
command: `codex --profile ${diagnostics.config.activeProfile || 'default'}`,
description: 'Inspect the active profile directly in native Codex.',
},
]}
/>
<TabsContent value="routing" className={tabContentClassName}>
<ScrollArea className="h-full">
<div className="space-y-4 pr-1">
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base">
@@ -507,46 +573,146 @@ export function CodexPage() {
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base">Quick usage</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<UsageCommand label="Codex alias (explicit)" command="ccs-codex" />
<UsageCommand label="Codex alias (short)" command="ccsx" />
<UsageCommand
label="Run a profile on native Codex"
command='ccs codex --target codex "your prompt"'
/>
<UsageCommand
label="Routed profile example"
command='ccs mycodex --target codex "your prompt"'
/>
</CardContent>
</Card>
{diagnostics.warnings.length > 0 && (
<Card className="border-amber-200 bg-amber-50/50 dark:bg-amber-950/20">
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base">
<AlertTriangle className="h-4 w-4 text-amber-600" />
Warnings
</CardTitle>
</CardHeader>
<CardContent className="space-y-1.5">
{diagnostics.warnings.map((warning) => (
<p key={warning} className="text-sm text-amber-800 dark:text-amber-300">
- {warning}
</p>
))}
</CardContent>
</Card>
)}
</div>
</ScrollArea>
</TabsContent>
<TabsContent value="controls" className={tabContentClassName}>
<ScrollArea className="h-full">
<div className="space-y-4 pr-1">
<Card>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2 text-base">
<FileWarning className="h-4 w-4" />
What this editor affects
<Route className="h-4 w-4" />
Structured controls boundary
</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>
Edits here affect native Codex sessions first because this is the user-layer{' '}
<code>config.toml</code>.
Guided controls write only the user-layer <code>config.toml</code>. They do
not model the full effective Codex runtime once trusted repo layers and CCS
transient <code>-c</code> overrides are involved.
</p>
<p>
CCS-routed Codex launches may override provider-related keys transiently and
inject <code>CCS_CODEX_API_KEY</code>.
</p>
<p>
That means the file is not a complete source of truth for every routed Codex
launch you start from CCS.
Structured saves normalize TOML formatting and strip comments. Use the raw
editor on the right when exact layout matters.
</p>
</CardContent>
</Card>
<CodexTopLevelControlsCard
values={topLevelSettings}
providerNames={modelProviderEntries.map((entry) => entry.name)}
disabled={structuredControlsDisabled}
disabledReason={controlsDisabledReason}
saving={isPatchingConfig}
onSave={(values) =>
runConfigPatch({ kind: 'top-level', values }, 'Saved top-level Codex settings.')
}
/>
<CodexProjectTrustCard
workspacePath={diagnostics.workspacePath}
entries={projectTrustEntries}
disabled={structuredControlsDisabled}
disabledReason={controlsDisabledReason}
saving={isPatchingConfig}
onSave={(projectPath, trustLevel) =>
runConfigPatch(
{ kind: 'project-trust', path: projectPath, trustLevel },
trustLevel ? 'Saved project trust entry.' : 'Removed project trust entry.'
)
}
/>
<CodexProfilesCard
activeProfile={diagnostics.config.activeProfile}
entries={profileEntries}
providerNames={modelProviderEntries.map((entry) => entry.name)}
disabled={structuredControlsDisabled}
disabledReason={controlsDisabledReason}
saving={isPatchingConfig}
onSave={(name, values, setAsActive) =>
runConfigPatch(
{ kind: 'profile', action: 'upsert', name, values, setAsActive },
'Saved profile.'
)
}
onDelete={(name) =>
runConfigPatch({ kind: 'profile', action: 'delete', name }, 'Deleted profile.')
}
onSetActive={(name) =>
runConfigPatch(
{ kind: 'profile', action: 'set-active', name },
'Set active profile.'
)
}
/>
<CodexModelProvidersCard
entries={modelProviderEntries}
disabled={structuredControlsDisabled}
disabledReason={controlsDisabledReason}
saving={isPatchingConfig}
onSave={(name, values) =>
runConfigPatch(
{ kind: 'model-provider', action: 'upsert', name, values },
'Saved model provider.'
)
}
onDelete={(name) =>
runConfigPatch(
{ kind: 'model-provider', action: 'delete', name },
'Deleted model provider.'
)
}
/>
<CodexMcpServersCard
entries={mcpServerEntries}
disabled={structuredControlsDisabled}
disabledReason={controlsDisabledReason}
saving={isPatchingConfig}
onSave={(name, values) =>
runConfigPatch(
{ kind: 'mcp-server', action: 'upsert', name, values },
'Saved MCP server.'
)
}
onDelete={(name) =>
runConfigPatch(
{ kind: 'mcp-server', action: 'delete', name },
'Deleted MCP server.'
)
}
/>
<CodexFeaturesCard
catalog={KNOWN_CODEX_FEATURES}
state={featureState}
disabled={structuredControlsDisabled}
disabledReason={controlsDisabledReason}
onToggle={(feature, enabled) =>
runConfigPatch({ kind: 'feature', feature, enabled }, 'Saved feature toggle.')
}
/>
</div>
</ScrollArea>
</TabsContent>