mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
Merge remote-tracking branch 'origin/dev' into kai/feat/867-image-analysis-backend-status
This commit is contained in:
commit
36acf9f190
17 files changed
+753
-138
No files matched your search
@@ -1,3 +1,47 @@
|
||||
## [7.64.0](https://github.com/kaitranntt/ccs/compare/v7.63.1...v7.64.0) (2026-04-01)
|
||||
|
||||
### Features
|
||||
|
||||
* add first-class CCS WebSearch runtime ([e260df7](https://github.com/kaitranntt/ccs/commit/e260df7178dd030efe3d490b4cd9421b386c43c2))
|
||||
* **cliproxy:** support duplicate-email codex accounts ([c73f338](https://github.com/kaitranntt/ccs/commit/c73f33872abc165aaaba8a240c06cc4ccbb6f5b4))
|
||||
* **ui:** unify duplicate-email account surfaces ([80341f1](https://github.com/kaitranntt/ccs/commit/80341f18c3c93dd4cdca92f6965136cff90be6f7))
|
||||
* **websearch:** add managed mcp runtime and provider cooldowns ([7f83e04](https://github.com/kaitranntt/ccs/commit/7f83e041b7fa8907a5d9484acd21ea88edac67e7))
|
||||
* **websearch:** finish managed third-party rollout ([de7171d](https://github.com/kaitranntt/ccs/commit/de7171d81076ecfd4e5e6474ce062c44a4c8be9a)), closes [#862](https://github.com/kaitranntt/ccs/issues/862)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bootstrap explicit CODEX_HOME for ccsxp ([b6717c5](https://github.com/kaitranntt/ccs/commit/b6717c55295dadeeff87ec356bc19d7b1b1d3733))
|
||||
* **ci:** harden ai review output ([4950be7](https://github.com/kaitranntt/ccs/commit/4950be7fc025877dd6744b9c8c87b4714ec49de2))
|
||||
* **codex:** harden duplicate-email account actions ([22f0916](https://github.com/kaitranntt/ccs/commit/22f091689fadf485d24fc46510f211a489680a2b))
|
||||
* **codex:** harden native runtime detection ([ca54bad](https://github.com/kaitranntt/ccs/commit/ca54bad2aa774c9dc3de9429d3e1440d179796a7))
|
||||
* **dashboard:** use shared base time for connection timeline events ([d76ef5b](https://github.com/kaitranntt/ccs/commit/d76ef5bcb30bd87ea487894586978473fcc8fead)), closes [#856](https://github.com/kaitranntt/ccs/issues/856)
|
||||
* **docker:** harden cliproxy local proxy with auth guard, dynamic port, and body handling ([6471cc5](https://github.com/kaitranntt/ccs/commit/6471cc55d72deeebcbca70d54983b90ae32c999a))
|
||||
* **docker:** harden proxy with timeout, Bun compat, and test coverage ([27409b7](https://github.com/kaitranntt/ccs/commit/27409b789b29a1d008f621f55aab7eed9861d6d4))
|
||||
* **docker:** proxy CLIProxy management panel through dashboard to avoid cross-origin errors ([881b061](https://github.com/kaitranntt/ccs/commit/881b061dfec497d7120b15d2045cf8e1a8f04bcc))
|
||||
* harden ai review comment formatting ([fa37f39](https://github.com/kaitranntt/ccs/commit/fa37f391c17c51c74a5bfe109225caf01798dc96))
|
||||
* merge existing disallowed tool flags for websearch ([b087c02](https://github.com/kaitranntt/ccs/commit/b087c02738c7e588491f84c8f2318c2ef5b03eca))
|
||||
* normalize websearch hook output for local providers ([a5d71e3](https://github.com/kaitranntt/ccs/commit/a5d71e35606ebaf16f97d860405800e9a9454659))
|
||||
* probe Codex config override support directly ([014a844](https://github.com/kaitranntt/ccs/commit/014a844844c76fe5dfce7763ed62e071ee063f58))
|
||||
* **ui:** normalize grouped account audience order ([498448a](https://github.com/kaitranntt/ccs/commit/498448a43181280dc06035d80b694f7522a1b28a))
|
||||
* use type-only import in instance manager ([f0cd44e](https://github.com/kaitranntt/ccs/commit/f0cd44e016c2c4a33ad06edcf6b13d8cbf1920bf))
|
||||
* **websearch:** close review gaps in managed runtime ([cae5b71](https://github.com/kaitranntt/ccs/commit/cae5b710b86a6bd985424cce028a502265051d09))
|
||||
|
||||
### Hotfixes
|
||||
|
||||
* restore prepack packaging contract ([78f6c5d](https://github.com/kaitranntt/ccs/commit/78f6c5d7156e1fb447e523b355c302a45d5b71bd))
|
||||
|
||||
### Documentation
|
||||
|
||||
* **architecture:** document codex duplicate-email identity ([be0c597](https://github.com/kaitranntt/ccs/commit/be0c597d255a21a7e935f033dfcc4de4e19c8c8e))
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* remove duplicate copilot websearch sync ([2c15304](https://github.com/kaitranntt/ccs/commit/2c153045f33e02cc821a623f6594fb4a6bebb321))
|
||||
|
||||
### Tests
|
||||
|
||||
* **dashboard:** add regression tests for connection timeline event generation ([4ab5194](https://github.com/kaitranntt/ccs/commit/4ab51945a412321e3e56d0c6860d0121761dcbfa))
|
||||
|
||||
## [7.63.1](https://github.com/kaitranntt/ccs/compare/v7.63.0...v7.63.1) (2026-04-01)
|
||||
|
||||
### Hotfixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CCS Project Roadmap
|
||||
|
||||
Last Updated: 2026-03-30
|
||||
Last Updated: 2026-04-01
|
||||
|
||||
Forward-looking roadmap documenting current priorities, GitHub issues, and future feature plans.
|
||||
|
||||
@@ -41,6 +41,7 @@ All major modularization work is complete. The codebase evolved from monolithic
|
||||
|
||||
### Recent Fixes
|
||||
|
||||
- **2026-04-01**: The `Compatible -> Codex CLI` dashboard now exposes manual long-context controls for `model_context_window` and `model_auto_compact_token_limit`. CCS reads and patches those upstream Codex config keys directly, adds official guidance that GPT-5.4 long context is experimental and opt-in, and keeps the behavior manual-only so the dashboard never auto-fills or auto-saves long-context values for the user.
|
||||
- **2026-03-30**: **#862** Third-party WebSearch now uses a first-class CCS-managed MCP tool path instead of relying on a denied native Anthropic `WebSearch` call as the normal UX. CCS provisions `ccs-websearch` into `~/.claude.json`, syncs it into isolated account configs when needed, suppresses native `WebSearch` on third-party launches, preserves the provider order `Exa -> Tavily -> Brave -> DuckDuckGo -> legacy CLI fallback`, and keeps the old hook runtime only as shared provider plumbing plus compatibility fallback. Uninstall cleanup now also removes the managed WebSearch MCP runtime.
|
||||
- **2026-03-28**: **#773** CCS now ships a dedicated `Compatible -> Codex CLI` dashboard route with a real split-view control center. The page detects the local Codex binary, keeps overview/docs guidance, and adds guided editors for the user-owned `~/.codex/config.toml` layer: top-level runtime defaults, project trust, profiles, model providers, MCP servers, and supported feature flags. Structured saves intentionally normalize TOML formatting and drop comments, so the raw editor remains the fidelity escape hatch. Follow-up fixes added immediate raw snapshot refresh, refresh/discard recovery for stale raw drafts, dirty raw-editor guarding for structured controls, project-trust path validation, read-only handling for unreadable config files, preservation of unsupported upstream values such as granular `approval_policy`, and feature reset-to-default support. CCS still warns that transient runtime overrides such as `codex -c key=value` and `CCS_CODEX_API_KEY` may change effective behavior without persisting into the file.
|
||||
- **2026-03-27**: WebSearch dashboard cards now manage Exa, Tavily, and Brave API keys inline instead of relying on a separate manual env step. CCS stores those secrets through `global_env`, reflects masked key state in `/api/websearch`, and counts dashboard-managed keys as ready in the WebSearch status flow.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kaitranntt/ccs",
|
||||
"version": "7.63.1",
|
||||
"version": "7.64.0",
|
||||
"description": "Claude Code Switch - Instant profile switching between Claude, GLM, Kimi, and more",
|
||||
"keywords": [
|
||||
"cli",
|
||||
|
||||
@@ -91,6 +91,8 @@ export interface CodexSupportMatrixEntry {
|
||||
export interface CodexUserConfigDiagnostics {
|
||||
model: string | null;
|
||||
modelReasoningEffort: string | null;
|
||||
modelContextWindow: number | null;
|
||||
modelAutoCompactTokenLimit: number | null;
|
||||
modelProvider: string | null;
|
||||
activeProfile: string | null;
|
||||
approvalPolicy: string | null;
|
||||
@@ -137,6 +139,8 @@ export interface CodexRawConfigResponse {
|
||||
export interface CodexTopLevelSettingsPatch {
|
||||
model?: string | null;
|
||||
modelReasoningEffort?: string | null;
|
||||
modelContextWindow?: number | null;
|
||||
modelAutoCompactTokenLimit?: number | null;
|
||||
modelProvider?: string | null;
|
||||
approvalPolicy?: string | null;
|
||||
sandboxMode?: string | null;
|
||||
|
||||
@@ -243,6 +243,18 @@ function applyTopLevelSettingsPatch(
|
||||
'model_reasoning_effort'
|
||||
);
|
||||
}
|
||||
if (hasOwn(values, 'modelContextWindow')) {
|
||||
setNumberField(target, 'model_context_window', values.modelContextWindow, {
|
||||
integer: true,
|
||||
min: 1,
|
||||
});
|
||||
}
|
||||
if (hasOwn(values, 'modelAutoCompactTokenLimit')) {
|
||||
setNumberField(target, 'model_auto_compact_token_limit', values.modelAutoCompactTokenLimit, {
|
||||
integer: true,
|
||||
min: 1,
|
||||
});
|
||||
}
|
||||
if (hasOwn(values, 'modelProvider')) {
|
||||
setStringField(target, 'model_provider', values.modelProvider);
|
||||
}
|
||||
@@ -778,6 +790,8 @@ export async function getCodexDashboardDiagnostics(): Promise<CodexDashboardDiag
|
||||
config: {
|
||||
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),
|
||||
activeProfile,
|
||||
approvalPolicy: summarizeApprovalPolicy(config?.approval_policy),
|
||||
|
||||
@@ -179,6 +179,8 @@ describe('codex-dashboard-service', () => {
|
||||
path.join(codexHome, 'config.toml'),
|
||||
`model = "gpt-5.4"
|
||||
profile = "work"
|
||||
model_context_window = 800000
|
||||
model_auto_compact_token_limit = 700000
|
||||
model_provider = "cliproxy"
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "danger-full-access"
|
||||
@@ -217,6 +219,8 @@ model = "gpt-5.4"
|
||||
expect(diagnostics.binary.installed).toBe(true);
|
||||
expect(diagnostics.binary.supportsConfigOverrides).toBe(true);
|
||||
expect(diagnostics.config.model).toBe('gpt-5.4');
|
||||
expect(diagnostics.config.modelContextWindow).toBe(800000);
|
||||
expect(diagnostics.config.modelAutoCompactTokenLimit).toBe(700000);
|
||||
expect(diagnostics.config.activeProfile).toBe('work');
|
||||
expect(diagnostics.config.modelProvider).toBe('cliproxy');
|
||||
expect(diagnostics.config.profileCount).toBe(1);
|
||||
@@ -376,6 +380,8 @@ bearer_token = "secret"
|
||||
values: {
|
||||
model: 'gpt-5.4',
|
||||
modelReasoningEffort: 'high',
|
||||
modelContextWindow: 800000,
|
||||
modelAutoCompactTokenLimit: 700000,
|
||||
approvalPolicy: 'never',
|
||||
sandboxMode: 'workspace-write',
|
||||
webSearch: 'cached',
|
||||
@@ -394,10 +400,14 @@ bearer_token = "secret"
|
||||
const diagnostics = await getCodexDashboardDiagnostics();
|
||||
expect(diagnostics.config.model).toBe('gpt-5.4');
|
||||
expect(diagnostics.config.modelReasoningEffort).toBe('high');
|
||||
expect(diagnostics.config.modelContextWindow).toBe(800000);
|
||||
expect(diagnostics.config.modelAutoCompactTokenLimit).toBe(700000);
|
||||
expect(diagnostics.config.toolOutputTokenLimit).toBe(12000);
|
||||
expect(diagnostics.config.personality).toBe('friendly');
|
||||
expect(diagnostics.config.projectTrust[0]?.path).toBe('/tmp/workspace-a');
|
||||
expect(result.rawText).toContain('model = "gpt-5.4"');
|
||||
expect(result.rawText).toContain('model_context_window = 800000');
|
||||
expect(result.rawText).toContain('model_auto_compact_token_limit = 700000');
|
||||
expect(result.config?.model).toBe('gpt-5.4');
|
||||
});
|
||||
|
||||
@@ -665,4 +675,24 @@ bearer_token = "secret"
|
||||
})
|
||||
).rejects.toThrow(CodexRawConfigValidationError);
|
||||
});
|
||||
|
||||
it('rejects invalid long-context values in structured top-level patches', async () => {
|
||||
await expect(
|
||||
patchCodexConfig({
|
||||
kind: 'top-level',
|
||||
values: {
|
||||
modelContextWindow: 0,
|
||||
},
|
||||
})
|
||||
).rejects.toThrow(CodexRawConfigValidationError);
|
||||
|
||||
await expect(
|
||||
patchCodexConfig({
|
||||
kind: 'top-level',
|
||||
values: {
|
||||
modelAutoCompactTokenLimit: 1.5,
|
||||
},
|
||||
})
|
||||
).rejects.toThrow(CodexRawConfigValidationError);
|
||||
});
|
||||
});
|
||||
@@ -3,9 +3,16 @@
|
||||
*/
|
||||
|
||||
import { AccountSurfaceCard } from '@/components/account/shared/account-surface-card';
|
||||
import { QuotaTooltipContent } from '@/components/shared/quota-tooltip-content';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { formatQuotaPercent, getProviderMinQuota, getQuotaFailureInfo, cn } from '@/lib/utils';
|
||||
import {
|
||||
cn,
|
||||
formatQuotaPercent,
|
||||
getProviderMinQuota,
|
||||
getProviderResetTime,
|
||||
getQuotaFailureInfo,
|
||||
} from '@/lib/utils';
|
||||
import { GripVertical, Loader2, Pause, Play } from 'lucide-react';
|
||||
import {
|
||||
useAccountQuota,
|
||||
@@ -207,36 +214,45 @@ export function AccountCard({
|
||||
hasGroupedVariants && showQuota
|
||||
? (account.variants ?? []).map((variant, index) => {
|
||||
const quotaQuery = variantQuotaQueries[index];
|
||||
const minQuota = getProviderMinQuota(account.provider, quotaQuery?.data);
|
||||
const quota = quotaQuery?.data;
|
||||
const minQuota = getProviderMinQuota(account.provider, quota);
|
||||
const resetTime = getProviderResetTime(account.provider, quota);
|
||||
const quotaLabel = minQuota !== null ? formatQuotaPercent(minQuota) : null;
|
||||
const quotaValue = quotaLabel !== null ? Number(quotaLabel) : null;
|
||||
const failureInfo = getQuotaFailureInfo(quotaQuery?.data);
|
||||
const failureInfo = getQuotaFailureInfo(quota);
|
||||
const label = variant.audienceLabel ?? variant.detailLabel ?? cleanEmail(variant.email);
|
||||
|
||||
return (
|
||||
<div key={variant.id} className="space-y-0.5">
|
||||
<div className="flex items-center justify-between gap-2 text-[8px]">
|
||||
<span className="text-muted-foreground/80 truncate">{label}</span>
|
||||
<span className="font-mono text-foreground/80 shrink-0">
|
||||
{quotaQuery?.isLoading
|
||||
? t('accountCard.quotaLoading')
|
||||
: quotaValue !== null
|
||||
? `${quotaLabel}%`
|
||||
: failureInfo?.label || t('accountCard.quotaUnavailable')}
|
||||
</span>
|
||||
</div>
|
||||
{quotaValue !== null && (
|
||||
<div className="w-full bg-muted dark:bg-zinc-800/50 h-1 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all',
|
||||
getCompactQuotaColor(quotaValue)
|
||||
)}
|
||||
style={{ width: `${quotaValue}%` }}
|
||||
/>
|
||||
<Tooltip key={variant.id}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="space-y-0.5 cursor-help">
|
||||
<div className="flex items-center justify-between gap-2 text-[8px]">
|
||||
<span className="text-muted-foreground/80 truncate">{label}</span>
|
||||
<span className="font-mono text-foreground/80 shrink-0">
|
||||
{quotaQuery?.isLoading
|
||||
? t('accountCard.quotaLoading')
|
||||
: quotaValue !== null
|
||||
? `${quotaLabel}%`
|
||||
: failureInfo?.label || t('accountCard.quotaUnavailable')}
|
||||
</span>
|
||||
</div>
|
||||
{quotaValue !== null && (
|
||||
<div className="w-full bg-muted dark:bg-zinc-800/50 h-1 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
'h-full rounded-full transition-all',
|
||||
getCompactQuotaColor(quotaValue)
|
||||
)}
|
||||
style={{ width: `${quotaValue}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-xs">
|
||||
<QuotaTooltipContent quota={quota} resetTime={resetTime} />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})
|
||||
: null;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Route } from 'lucide-react';
|
||||
import { FileCode2, History, PenLine, Settings2 } from 'lucide-react';
|
||||
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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import type {
|
||||
CodexConfigPatchInput,
|
||||
@@ -54,26 +53,60 @@ export function CodexControlCenterTab({
|
||||
}: CodexControlCenterTabProps) {
|
||||
return (
|
||||
<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">
|
||||
<Route className="h-4 w-4" />
|
||||
Structured controls boundary
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>
|
||||
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>
|
||||
Structured saves normalize TOML formatting and strip comments. Use the raw editor on
|
||||
the right when exact layout matters.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="space-y-6 pr-1 pb-6">
|
||||
<div className="group relative overflow-hidden rounded-xl border border-border/80 bg-background/50 p-5 shadow-sm transition-all hover:bg-background hover:shadow-md dark:border-border/60">
|
||||
<div className="absolute inset-x-0 -top-px h-px bg-gradient-to-r from-transparent via-foreground/15 to-transparent transition-opacity group-hover:via-foreground/30"></div>
|
||||
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="flex-1 space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg border border-primary/20 bg-primary/10 text-primary transition-colors group-hover:border-primary/30">
|
||||
<Settings2 className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-base font-semibold tracking-tight text-foreground">
|
||||
Structured controls boundary
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="grid gap-3 sm:grid-cols-2 text-sm text-muted-foreground">
|
||||
<li className="flex items-start gap-2.5">
|
||||
<PenLine className="h-4 w-4 shrink-0 text-muted-foreground/60 mt-0.5" />
|
||||
<span className="leading-relaxed">
|
||||
Writes exclusively to user-layer{' '}
|
||||
<code className="text-[11px] bg-muted/70 px-1.5 py-0.5 rounded border border-border/50">
|
||||
config.toml
|
||||
</code>
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2.5">
|
||||
<History className="h-4 w-4 shrink-0 text-muted-foreground/60 mt-0.5" />
|
||||
<span className="leading-relaxed">
|
||||
Does not reflect repo trust layers or CLI overrides
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 lg:w-[280px]">
|
||||
<div className="relative overflow-hidden rounded-lg border border-amber-500/20 bg-amber-500/5 p-4 transition-colors group-hover:border-amber-500/30 group-hover:bg-amber-500/10 dark:border-amber-400/10 dark:bg-amber-400/5">
|
||||
<div className="flex items-start gap-3">
|
||||
<FileCode2 className="mt-0.5 h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
|
||||
<div className="space-y-1">
|
||||
<p className="text-[11px] font-bold text-amber-700 dark:text-amber-300 uppercase tracking-wider">
|
||||
Formatting Note
|
||||
</p>
|
||||
<p className="text-[13px] leading-relaxed text-amber-800/80 dark:text-amber-200/70">
|
||||
Saves normalize TOML formatting and strip comments. Switch to the raw editor
|
||||
if exact layout matters.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CodexTopLevelControlsCard
|
||||
values={topLevelSettings}
|
||||
|
||||
@@ -107,25 +107,29 @@ export function CodexDocsTab({ diagnostics }: CodexDocsTabProps) {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||
<p>
|
||||
Use <code>ccsxp</code> if you want the built-in CCS Codex provider shortcut on native
|
||||
Codex. Use the saved recipe below if you want plain <code>codex</code> or a personal
|
||||
alias like <code>cxp</code> to default to CLIProxy.
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
<p>
|
||||
<strong>Built-in:</strong> Use <code>ccsxp</code> for the CCS provider shortcut.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Native:</strong> Configure the recipe below to use CLIProxy directly with{' '}
|
||||
<code>codex</code>.
|
||||
</p>
|
||||
</div>
|
||||
<pre className="overflow-x-auto rounded-md border bg-muted/20 p-3 text-xs text-foreground">
|
||||
{CLIPROXY_NATIVE_CODEX_RECIPE}
|
||||
</pre>
|
||||
<div className="space-y-1">
|
||||
<p>
|
||||
1. Save the <code>cliproxy</code> provider in your user config.
|
||||
</p>
|
||||
<p>
|
||||
2. Set top-level <code>model_provider</code> to <code>cliproxy</code>.
|
||||
</p>
|
||||
<p>
|
||||
3. Export <code>CLIPROXY_API_KEY</code> in your shell before launching native Codex.
|
||||
</p>
|
||||
</div>
|
||||
<ol className="ml-4 list-decimal space-y-1.5 [&>li]:pl-1">
|
||||
<li>
|
||||
Save the <code>cliproxy</code> provider in your user config.
|
||||
</li>
|
||||
<li>
|
||||
Set top-level <code>model_provider</code> to <code>cliproxy</code>.
|
||||
</li>
|
||||
<li>
|
||||
Export <code>CLIPROXY_API_KEY</code> before launching native Codex.
|
||||
</li>
|
||||
</ol>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -137,11 +141,13 @@ export function CodexDocsTab({ diagnostics }: CodexDocsTabProps) {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm">
|
||||
{docsReference.notes.map((note, index) => (
|
||||
<p key={`${index}-${note}`} className="text-muted-foreground">
|
||||
- {renderTextWithLinks(note)}
|
||||
</p>
|
||||
))}
|
||||
{docsReference.notes.length > 0 && (
|
||||
<ul className="ml-4 list-disc space-y-1.5 text-muted-foreground [&>li]:pl-1">
|
||||
{docsReference.notes.map((note, index) => (
|
||||
<li key={`${index}-${note}`}>{renderTextWithLinks(note)}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs uppercase tracking-wide text-muted-foreground">Codex docs</p>
|
||||
|
||||
@@ -74,31 +74,23 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
How Codex works in CCS
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>Codex is a first-class runtime target in CCS, but it stays runtime-only in v1.</p>
|
||||
<p>
|
||||
<code>ccs-codex</code> and <code>ccsx</code> launch native Codex against your saved
|
||||
native config, while <code>ccsxp</code> is the opinionated shortcut for{' '}
|
||||
<code>ccs codex --target codex</code>.
|
||||
</p>
|
||||
<p>
|
||||
Plain <code>codex</code> or a personal alias like <code>cxp</code> needs{' '}
|
||||
<code>model_provider = "cliproxy"</code> plus a matching{' '}
|
||||
<code>[model_providers.cliproxy]</code> entry if you want CLIProxy as the saved native
|
||||
default.
|
||||
</p>
|
||||
<p>
|
||||
Built-in <code>openai</code> and <code>oss</code> providers are also valid native
|
||||
defaults and do not need a custom <code>[model_providers]</code> stanza.
|
||||
</p>
|
||||
<p>
|
||||
Saved default targets for API profiles and variants still remain on Claude or Droid.
|
||||
</p>
|
||||
<p>
|
||||
CCS-backed Codex launches can apply transient <code>-c</code> overrides and inject
|
||||
<code> CCS_CODEX_API_KEY</code>, so effective runtime values may not match this file
|
||||
exactly.
|
||||
</p>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
<ul className="ml-4 list-disc space-y-1.5 [&>li]:pl-1">
|
||||
<li>Codex is a first-class, runtime-only target in CCS v1.</li>
|
||||
<li>
|
||||
<strong>Native config:</strong> <code>ccs-codex</code> and <code>ccsx</code> launch
|
||||
native Codex using your saved defaults.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Transient overrides:</strong> <code>ccsxp</code> (or{' '}
|
||||
<code>ccs codex --target codex</code>) uses the CCS provider shortcut.
|
||||
</li>
|
||||
<li>
|
||||
<strong>CLIProxy default:</strong> To make plain <code>codex</code> use CLIProxy,
|
||||
set <code>model_provider = "cliproxy"</code> and add the recipe below.
|
||||
</li>
|
||||
<li>API profiles continue to default to Claude or Droid.</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -145,32 +137,40 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||
{supportsManagedRouting ? (
|
||||
<>
|
||||
<p>
|
||||
There are two supported paths. Use <code>ccsxp</code> if you want the built-in CCS
|
||||
Codex provider shortcut. Use the saved recipe below if you want plain{' '}
|
||||
<code>codex</code> or a personal alias like <code>cxp</code> to default to
|
||||
CLIProxy.
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
<p>
|
||||
<strong>Two supported paths:</strong>
|
||||
</p>
|
||||
<ul className="ml-4 list-disc space-y-1 [&>li]:pl-1">
|
||||
<li>
|
||||
<strong>Built-in:</strong> Use <code>ccsxp</code> for the CCS provider
|
||||
shortcut.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Native:</strong> Configure the recipe below to use CLIProxy directly
|
||||
with <code>codex</code>.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="rounded-md border bg-muted/20 p-3">
|
||||
<p className="font-medium text-foreground">Saved native Codex recipe</p>
|
||||
<pre className="mt-2 overflow-x-auto rounded-md bg-background p-3 text-xs text-foreground">
|
||||
{CLIPROXY_NATIVE_CODEX_RECIPE}
|
||||
</pre>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p>
|
||||
1. Save a provider named <code>cliproxy</code> with the base URL and env key
|
||||
above.
|
||||
</p>
|
||||
<p>
|
||||
2. In <strong>Top-level settings</strong>, set <strong>Default provider</strong>{' '}
|
||||
to <code>cliproxy</code>.
|
||||
</p>
|
||||
<p>
|
||||
3. Export <code>CLIPROXY_API_KEY</code> in your shell before launching native
|
||||
<ol className="ml-4 list-decimal space-y-1.5 [&>li]:pl-1">
|
||||
<li>
|
||||
Save a provider named <code>cliproxy</code> with the base URL and env key above.
|
||||
</li>
|
||||
<li>
|
||||
In <strong>Top-level settings</strong>, set <strong>Default provider</strong> to{' '}
|
||||
<code>cliproxy</code>.
|
||||
</li>
|
||||
<li>
|
||||
Export <code>CLIPROXY_API_KEY</code> in your shell before launching native
|
||||
Codex.
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
@@ -320,30 +320,44 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-3 md:grid-cols-2">
|
||||
<div className="rounded-md border p-3 text-sm">
|
||||
<p className="font-medium">Native Codex runtime</p>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
Use <code>ccs-codex</code>, <code>ccsx</code>, or <code>--target codex</code> when
|
||||
you want the local Codex CLI to honor your saved native user config.
|
||||
</p>
|
||||
<div className="flex flex-col rounded-md border p-3 text-sm">
|
||||
<p className="font-medium text-foreground">Native Codex runtime</p>
|
||||
<ul className="mt-2 flex-grow list-disc space-y-1.5 pl-4 text-muted-foreground [&>li]:pl-1">
|
||||
<li>
|
||||
<code>ccs-codex</code>
|
||||
</li>
|
||||
<li>
|
||||
<code>ccsx</code>
|
||||
</li>
|
||||
<li>
|
||||
<code>--target codex</code>
|
||||
</li>
|
||||
</ul>
|
||||
<Badge variant="secondary" className="mt-4 w-fit justify-center font-normal">
|
||||
Honors saved native user config
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="rounded-md border p-3 text-sm">
|
||||
<p className="font-medium">CCS Codex provider / bridge</p>
|
||||
<p className="mt-1 text-muted-foreground">
|
||||
{supportsManagedRouting ? (
|
||||
<>
|
||||
Use <code>ccsxp</code> or <code>ccs codex --target codex</code> when you want
|
||||
the built-in CCS Codex provider on native Codex. That path uses transient
|
||||
CCS-managed overrides and is separate from the saved <code>cliproxy</code>{' '}
|
||||
recipe above.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
The CCS Codex provider route is currently unavailable because the detected Codex
|
||||
build does not expose <code>--config</code> overrides.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<div className="flex flex-col rounded-md border p-3 text-sm">
|
||||
<p className="font-medium text-foreground">CCS Codex provider / bridge</p>
|
||||
{supportsManagedRouting ? (
|
||||
<>
|
||||
<ul className="mt-2 flex-grow list-disc space-y-1.5 pl-4 text-muted-foreground [&>li]:pl-1">
|
||||
<li>
|
||||
<code>ccsxp</code>
|
||||
</li>
|
||||
<li>
|
||||
<code>ccs codex --target codex</code>
|
||||
</li>
|
||||
</ul>
|
||||
<Badge variant="secondary" className="mt-4 w-fit justify-center font-normal">
|
||||
Uses transient overrides
|
||||
</Badge>
|
||||
</>
|
||||
) : (
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
Unavailable (Codex build lacks <code>--config</code> support).
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Loader2, SlidersHorizontal } from 'lucide-react';
|
||||
import { CircleAlert, Loader2, SlidersHorizontal } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
@@ -14,6 +15,11 @@ import type { CodexTopLevelSettingsView } from '@/lib/codex-config';
|
||||
import { CodexConfigCardShell } from './codex-config-card-shell';
|
||||
|
||||
const UNSET = '__unset__';
|
||||
const GPT_54_MAX_CONTEXT_WINDOW = 1_050_000;
|
||||
const GPT_54_STANDARD_CONTEXT_WINDOW = 272_000;
|
||||
const CCS_GPT_54_STARTER_CONTEXT_WINDOW = 800_000;
|
||||
const CCS_GPT_54_STARTER_AUTO_COMPACT_TOKEN_LIMIT = 700_000;
|
||||
const INTEGER_FORMATTER = new Intl.NumberFormat('en-US');
|
||||
|
||||
interface CodexTopLevelControlsCardProps {
|
||||
values: CodexTopLevelSettingsView;
|
||||
@@ -32,6 +38,14 @@ function withCurrentValue(options: string[], current: string | null | undefined)
|
||||
return current && !options.includes(current) ? [current, ...options] : options;
|
||||
}
|
||||
|
||||
function formatInteger(value: number) {
|
||||
return INTEGER_FORMATTER.format(value);
|
||||
}
|
||||
|
||||
function isGpt54ModelId(value: string | null | undefined) {
|
||||
return value?.trim().toLowerCase().startsWith('gpt-5.4') ?? false;
|
||||
}
|
||||
|
||||
function buildTopLevelPatch(
|
||||
initialValues: CodexTopLevelSettingsView,
|
||||
draft: CodexTopLevelSettingsView
|
||||
@@ -42,6 +56,12 @@ function buildTopLevelPatch(
|
||||
if (draft.modelReasoningEffort !== initialValues.modelReasoningEffort) {
|
||||
patch.modelReasoningEffort = draft.modelReasoningEffort;
|
||||
}
|
||||
if (draft.modelContextWindow !== initialValues.modelContextWindow) {
|
||||
patch.modelContextWindow = draft.modelContextWindow;
|
||||
}
|
||||
if (draft.modelAutoCompactTokenLimit !== initialValues.modelAutoCompactTokenLimit) {
|
||||
patch.modelAutoCompactTokenLimit = draft.modelAutoCompactTokenLimit;
|
||||
}
|
||||
if (draft.modelProvider !== initialValues.modelProvider) {
|
||||
patch.modelProvider = draft.modelProvider;
|
||||
}
|
||||
@@ -91,6 +111,11 @@ function TopLevelControlsForm({
|
||||
const personalityOptions = withCurrentValue(['none', 'friendly', 'pragmatic'], draft.personality);
|
||||
const patch = buildTopLevelPatch(initialValues, draft);
|
||||
const hasChanges = Object.keys(patch).length > 0;
|
||||
const isGpt54Selected = isGpt54ModelId(draft.model);
|
||||
const parseOptionalInteger = (value: string) => {
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? Number(trimmed) : null;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -266,6 +291,216 @@ function TopLevelControlsForm({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 rounded-xl border border-amber-500/30 bg-amber-500/5 p-4 shadow-sm dark:bg-amber-400/5">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="space-y-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<CircleAlert className="h-4 w-4 text-amber-600 dark:text-amber-300" />
|
||||
<p className="text-sm font-semibold">Long context override</p>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-amber-500/40 bg-background/80 text-[10px] uppercase tracking-[0.16em] text-amber-700 dark:text-amber-300"
|
||||
>
|
||||
Manual opt-in only
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-[10px] uppercase tracking-[0.16em] text-muted-foreground"
|
||||
>
|
||||
{isGpt54Selected ? 'GPT-5.4 selected' : 'GPT-5.4 reference'}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Draft values only. Nothing applies until Save.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
modelContextWindow: CCS_GPT_54_STARTER_CONTEXT_WINDOW,
|
||||
modelAutoCompactTokenLimit: CCS_GPT_54_STARTER_AUTO_COMPACT_TOKEN_LIMIT,
|
||||
}))
|
||||
}
|
||||
>
|
||||
Fill cautious pair
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
modelContextWindow: GPT_54_MAX_CONTEXT_WINDOW,
|
||||
}))
|
||||
}
|
||||
>
|
||||
Set official max window
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
onClick={() =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
modelContextWindow: null,
|
||||
modelAutoCompactTokenLimit: null,
|
||||
}))
|
||||
}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
<div className="rounded-lg border bg-background/85 px-3 py-3 shadow-sm shadow-black/5">
|
||||
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Official max
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-base font-semibold text-foreground">1.05M / 1M</p>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">GPT-5.4 context cap</p>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-background/85 px-3 py-3 shadow-sm shadow-black/5">
|
||||
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Standard window
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-base font-semibold text-foreground">
|
||||
{formatInteger(GPT_54_STANDARD_CONTEXT_WINDOW)}
|
||||
</p>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">Normal usage window</p>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-background/85 px-3 py-3 shadow-sm shadow-black/5">
|
||||
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
Above 272K
|
||||
</p>
|
||||
<p className="mt-1 font-mono text-base font-semibold text-foreground">Counts 2x</p>
|
||||
<p className="mt-1 text-[11px] text-muted-foreground">Usage-limit cost above 272K</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 rounded-lg border bg-background/75 px-3 py-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<p className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
One cautious pair
|
||||
</p>
|
||||
<div className="rounded-full border bg-background px-2.5 py-1 font-mono text-[11px] font-medium">
|
||||
Context {formatInteger(CCS_GPT_54_STARTER_CONTEXT_WINDOW)}
|
||||
</div>
|
||||
<div className="rounded-full border bg-background px-2.5 py-1 font-mono text-[11px] font-medium">
|
||||
Auto-compact {formatInteger(CCS_GPT_54_STARTER_AUTO_COMPACT_TOKEN_LIMIT)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-border/70 bg-background/80 text-[10px] uppercase tracking-[0.14em] text-muted-foreground"
|
||||
>
|
||||
Not official
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-border/70 bg-background/80 text-[10px] uppercase tracking-[0.14em] text-muted-foreground"
|
||||
>
|
||||
Draft only
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
|
||||
<span>Quick-fill only. Review before saving.</span>
|
||||
{!isGpt54Selected && draft.model ? (
|
||||
<span>
|
||||
<code>{draft.model}</code> should be checked separately.
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-medium">Model context window</p>
|
||||
<Input
|
||||
aria-label="Model context window"
|
||||
type="number"
|
||||
min={1}
|
||||
value={draft.modelContextWindow ?? ''}
|
||||
onChange={(event) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
modelContextWindow: parseOptionalInteger(event.target.value),
|
||||
}))
|
||||
}
|
||||
placeholder="Unset"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Writes <code>model_context_window</code>. Leave unset to keep Codex defaults.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-medium">Auto-compact token limit</p>
|
||||
<Input
|
||||
aria-label="Auto-compact token limit"
|
||||
type="number"
|
||||
min={1}
|
||||
value={draft.modelAutoCompactTokenLimit ?? ''}
|
||||
onChange={(event) =>
|
||||
setDraft((current) => ({
|
||||
...current,
|
||||
modelAutoCompactTokenLimit: parseOptionalInteger(event.target.value),
|
||||
}))
|
||||
}
|
||||
placeholder="Unset"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Writes <code>model_auto_compact_token_limit</code>. Leave unset to keep model
|
||||
defaults.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
|
||||
<span className="text-[10px] uppercase tracking-[0.14em]">Docs</span>
|
||||
<a
|
||||
href="https://developers.openai.com/api/docs/models/gpt-5.4"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
GPT-5.4 model page
|
||||
</a>
|
||||
<a
|
||||
href="https://openai.com/index/introducing-gpt-5-4/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
Release notes
|
||||
</a>
|
||||
<a
|
||||
href="https://developers.openai.com/codex/config-reference"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="underline underline-offset-2 hover:text-foreground"
|
||||
>
|
||||
Config reference
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={() => onSave(patch)} disabled={disabled || saving || !hasChanges}>
|
||||
{saving ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export interface CodexTopLevelSettingsView {
|
||||
model: string | null;
|
||||
modelReasoningEffort: string | null;
|
||||
modelContextWindow: number | null;
|
||||
modelAutoCompactTokenLimit: number | null;
|
||||
modelProvider: string | null;
|
||||
approvalPolicy: string | null;
|
||||
sandboxMode: string | null;
|
||||
@@ -125,6 +127,8 @@ export function readCodexTopLevelSettings(
|
||||
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),
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen, userEvent } from '@tests/setup/test-utils';
|
||||
import { AccountCard } from '@/components/account/flow-viz/account-card';
|
||||
import type { AccountData } from '@/components/account/flow-viz/types';
|
||||
import type { CodexQuotaResult } from '@/lib/api-client';
|
||||
import { useAccountQuota, useAccountQuotas } from '@/hooks/use-cliproxy-stats';
|
||||
|
||||
vi.mock('@/hooks/use-cliproxy-stats', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/hooks/use-cliproxy-stats')>(
|
||||
'@/hooks/use-cliproxy-stats'
|
||||
);
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useAccountQuota: vi.fn(),
|
||||
useAccountQuotas: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockedUseAccountQuota = vi.mocked(useAccountQuota);
|
||||
const mockedUseAccountQuotas = vi.mocked(useAccountQuotas);
|
||||
|
||||
function makeCodexQuota(planType: 'plus' | 'team', fiveHour: number, weekly: number) {
|
||||
return {
|
||||
success: true,
|
||||
planType,
|
||||
lastUpdated: Date.now(),
|
||||
windows: [
|
||||
{
|
||||
label: 'Primary',
|
||||
usedPercent: 100 - fiveHour,
|
||||
remainingPercent: fiveHour,
|
||||
resetAfterSeconds: 60 * 60,
|
||||
resetAt: '2026-04-04T08:44:00Z',
|
||||
},
|
||||
{
|
||||
label: 'Secondary',
|
||||
usedPercent: 100 - weekly,
|
||||
remainingPercent: weekly,
|
||||
resetAfterSeconds: 7 * 24 * 60 * 60,
|
||||
resetAt: '2026-04-08T10:20:00Z',
|
||||
},
|
||||
],
|
||||
coreUsage: {
|
||||
fiveHour: {
|
||||
label: 'Primary',
|
||||
remainingPercent: fiveHour,
|
||||
resetAfterSeconds: 60 * 60,
|
||||
resetAt: '2026-04-04T08:44:00Z',
|
||||
},
|
||||
weekly: {
|
||||
label: 'Secondary',
|
||||
remainingPercent: weekly,
|
||||
resetAfterSeconds: 7 * 24 * 60 * 60,
|
||||
resetAt: '2026-04-08T10:20:00Z',
|
||||
},
|
||||
},
|
||||
} satisfies CodexQuotaResult;
|
||||
}
|
||||
|
||||
const groupedAccount: AccountData = {
|
||||
id: 'codex:user@example.com',
|
||||
email: 'user@example.com',
|
||||
tokenFile: 'codex-user.json',
|
||||
provider: 'codex',
|
||||
successCount: 9,
|
||||
failureCount: 1,
|
||||
color: '#1e6091',
|
||||
variants: [
|
||||
{
|
||||
id: 'business@example.com',
|
||||
email: 'user@example.com',
|
||||
tokenFile: 'codex-business.json',
|
||||
isDefault: false,
|
||||
successCount: 5,
|
||||
failureCount: 0,
|
||||
audience: 'business',
|
||||
audienceLabel: 'Business',
|
||||
detailLabel: null,
|
||||
},
|
||||
{
|
||||
id: 'personal@example.com',
|
||||
email: 'user@example.com',
|
||||
tokenFile: 'codex-personal.json',
|
||||
isDefault: true,
|
||||
successCount: 4,
|
||||
failureCount: 1,
|
||||
audience: 'personal',
|
||||
audienceLabel: 'Personal',
|
||||
detailLabel: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('AccountCard grouped quota tooltip', () => {
|
||||
beforeEach(() => {
|
||||
mockedUseAccountQuota.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
} as ReturnType<typeof useAccountQuota>);
|
||||
|
||||
mockedUseAccountQuotas.mockReturnValue([
|
||||
{
|
||||
data: makeCodexQuota('team', 95, 81),
|
||||
isLoading: false,
|
||||
},
|
||||
{
|
||||
data: makeCodexQuota('plus', 64, 42),
|
||||
isLoading: false,
|
||||
},
|
||||
] as ReturnType<typeof useAccountQuotas>);
|
||||
});
|
||||
|
||||
it('shows provider quota tooltip content for each grouped personal/business row on hover', async () => {
|
||||
render(
|
||||
<AccountCard
|
||||
account={groupedAccount}
|
||||
zone="left"
|
||||
originalIndex={0}
|
||||
isHovered={false}
|
||||
isDragging={false}
|
||||
offset={{ x: 0, y: 0 }}
|
||||
showDetails={false}
|
||||
privacyMode={false}
|
||||
onMouseEnter={() => undefined}
|
||||
onMouseLeave={() => undefined}
|
||||
onPointerDown={() => undefined}
|
||||
onPointerMove={() => undefined}
|
||||
onPointerUp={() => undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.hover(screen.getByText('Business'));
|
||||
expect((await screen.findAllByText('Plan: team')).length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('5h usage limit').length).toBeGreaterThan(0);
|
||||
|
||||
await userEvent.hover(screen.getByText('Personal'));
|
||||
expect((await screen.findAllByText('Plan: plus')).length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('Weekly usage limit').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -30,6 +30,8 @@ function buildDiagnostics(activeProfile: string | null): CodexDashboardDiagnosti
|
||||
config: {
|
||||
model: 'gpt-5.4',
|
||||
modelReasoningEffort: null,
|
||||
modelContextWindow: null,
|
||||
modelAutoCompactTokenLimit: null,
|
||||
modelProvider: 'openai',
|
||||
activeProfile,
|
||||
approvalPolicy: null,
|
||||
|
||||
@@ -11,6 +11,8 @@ describe('CodexTopLevelControlsCard', () => {
|
||||
values={{
|
||||
model: null,
|
||||
modelReasoningEffort: null,
|
||||
modelContextWindow: null,
|
||||
modelAutoCompactTokenLimit: null,
|
||||
modelProvider: null,
|
||||
approvalPolicy: null,
|
||||
sandboxMode: null,
|
||||
@@ -34,4 +36,69 @@ describe('CodexTopLevelControlsCard', () => {
|
||||
expect(onSave).toHaveBeenCalledTimes(1);
|
||||
expect(onSave).toHaveBeenCalledWith({ model: 'gpt-5.4-mini' });
|
||||
});
|
||||
|
||||
it('submits manual long-context overrides without auto-filling defaults', async () => {
|
||||
const onSave = vi.fn();
|
||||
|
||||
render(
|
||||
<CodexTopLevelControlsCard
|
||||
values={{
|
||||
model: 'gpt-5.4',
|
||||
modelReasoningEffort: null,
|
||||
modelContextWindow: null,
|
||||
modelAutoCompactTokenLimit: null,
|
||||
modelProvider: null,
|
||||
approvalPolicy: null,
|
||||
sandboxMode: null,
|
||||
webSearch: null,
|
||||
toolOutputTokenLimit: null,
|
||||
personality: null,
|
||||
}}
|
||||
providerNames={[]}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Model context window'), '800000');
|
||||
await userEvent.type(screen.getByLabelText('Auto-compact token limit'), '700000');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Save top-level settings' }));
|
||||
|
||||
expect(onSave).toHaveBeenCalledWith({
|
||||
modelContextWindow: 800000,
|
||||
modelAutoCompactTokenLimit: 700000,
|
||||
});
|
||||
});
|
||||
|
||||
it('fills draft starter values without saving automatically', async () => {
|
||||
const onSave = vi.fn();
|
||||
|
||||
render(
|
||||
<CodexTopLevelControlsCard
|
||||
values={{
|
||||
model: 'gpt-5.4',
|
||||
modelReasoningEffort: null,
|
||||
modelContextWindow: null,
|
||||
modelAutoCompactTokenLimit: null,
|
||||
modelProvider: null,
|
||||
approvalPolicy: null,
|
||||
sandboxMode: null,
|
||||
webSearch: null,
|
||||
toolOutputTokenLimit: null,
|
||||
personality: null,
|
||||
}}
|
||||
providerNames={[]}
|
||||
onSave={onSave}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Manual opt-in only')).toBeInTheDocument();
|
||||
expect(screen.getByText('1.05M / 1M')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Fill cautious pair' }));
|
||||
|
||||
expect(onSave).not.toHaveBeenCalled();
|
||||
expect(screen.getByLabelText('Model context window')).toHaveValue(800000);
|
||||
expect(screen.getByLabelText('Auto-compact token limit')).toHaveValue(700000);
|
||||
expect(screen.getByRole('button', { name: 'Save top-level settings' })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
@@ -37,6 +37,8 @@ const diagnosticsResponse = {
|
||||
config: {
|
||||
model: 'gpt-5.3-codex',
|
||||
modelReasoningEffort: null,
|
||||
modelContextWindow: null,
|
||||
modelAutoCompactTokenLimit: null,
|
||||
modelProvider: null,
|
||||
activeProfile: null,
|
||||
approvalPolicy: null,
|
||||
|
||||
@@ -79,6 +79,8 @@ const diagnostics = {
|
||||
config: {
|
||||
model: 'gpt-5.4',
|
||||
modelReasoningEffort: null,
|
||||
modelContextWindow: null,
|
||||
modelAutoCompactTokenLimit: null,
|
||||
modelProvider: null,
|
||||
activeProfile: null,
|
||||
approvalPolicy: null,
|
||||
|
||||
Reference in new issue
Block a user