mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-17 12:15:51 +00:00
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:
1 parent
3c52b1ab6d
commit
b47aa0d28d
18 files changed
+2959
-57
No files matched your search
+220
-54
@@ -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 & 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>
|
||||
|
||||
Reference in new issue
Block a user