From 20b7bae046ecbee1b7a5d0e6ec373035e4ab7786 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 4 Dec 2025 05:29:55 -0500 Subject: [PATCH] refactor(cliproxy): use centralized ui system for model config display Replace custom formatting with ui.ts imports (color, bold, dim, ok, info, header, initUI) to maintain consistent styling with ccs doctor. Add model descriptions to catalog and clarify [Paid API] tier labeling. Update showCurrentConfig to async for UI initialization. Improves code reuse and visual consistency across CLI commands. --- src/cliproxy/model-catalog.ts | 42 +++++++++++++--- src/cliproxy/model-config.ts | 62 +++++++++++++++++------- tests/unit/cliproxy/model-config.test.js | 14 +++--- 3 files changed, 86 insertions(+), 32 deletions(-) diff --git a/src/cliproxy/model-catalog.ts b/src/cliproxy/model-catalog.ts index 557cf828..0fccc017 100644 --- a/src/cliproxy/model-catalog.ts +++ b/src/cliproxy/model-catalog.ts @@ -15,8 +15,10 @@ export interface ModelEntry { id: string; /** Human-readable name for display */ name: string; - /** Access tier indicator */ + /** Access tier indicator - 'paid' means requires paid API tier (not model pricing) */ tier?: 'free' | 'paid'; + /** Optional description for the model */ + description?: string; } /** @@ -40,10 +42,27 @@ export const MODEL_CATALOG: Partial> = displayName: 'Antigravity', defaultModel: 'gemini-3-pro-preview', models: [ - { id: 'gemini-claude-opus-4-5-thinking', name: 'Claude Opus 4.5 Thinking' }, - { id: 'gemini-claude-sonnet-4-5-thinking', name: 'Claude Sonnet 4.5 Thinking' }, - { id: 'gemini-claude-sonnet-4-5', name: 'Claude Sonnet 4.5' }, - { id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro', tier: 'paid' }, + { + id: 'gemini-claude-opus-4-5-thinking', + name: 'Claude Opus 4.5 Thinking', + description: 'Most capable, extended thinking', + }, + { + id: 'gemini-claude-sonnet-4-5-thinking', + name: 'Claude Sonnet 4.5 Thinking', + description: 'Balanced with extended thinking', + }, + { + id: 'gemini-claude-sonnet-4-5', + name: 'Claude Sonnet 4.5', + description: 'Fast and capable', + }, + { + id: 'gemini-3-pro-preview', + name: 'Gemini 3 Pro', + tier: 'paid', + description: 'Google latest, requires Paid API tier', + }, ], }, gemini: { @@ -51,8 +70,17 @@ export const MODEL_CATALOG: Partial> = displayName: 'Gemini', defaultModel: 'gemini-2.5-pro', models: [ - { id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro', tier: 'paid' }, - { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro' }, + { + id: 'gemini-3-pro-preview', + name: 'Gemini 3 Pro', + tier: 'paid', + description: 'Latest model, requires Paid API tier', + }, + { + id: 'gemini-2.5-pro', + name: 'Gemini 2.5 Pro', + description: 'Stable release, works with free tier', + }, ], }, }; diff --git a/src/cliproxy/model-config.ts b/src/cliproxy/model-config.ts index 1ff67b97..d58c6c11 100644 --- a/src/cliproxy/model-config.ts +++ b/src/cliproxy/model-config.ts @@ -11,6 +11,7 @@ import { InteractivePrompt } from '../utils/prompt'; import { getProviderCatalog, supportsModelConfig, ModelEntry } from './model-catalog'; import { getProviderSettingsPath, getClaudeEnvVars } from './config-generator'; import { CLIProxyProvider } from './types'; +import { initUI, color, bold, dim, ok, info, header } from '../utils/ui'; /** CCS directory */ const CCS_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.ccs'); @@ -39,11 +40,23 @@ export function getCurrentModel(provider: CLIProxyProvider): string | undefined } /** - * Format model entry for display + * Format model entry for display in selection list */ function formatModelOption(model: ModelEntry): string { - const tierLabel = model.tier === 'paid' ? ' [paid]' : ''; - return `${model.name} (${model.id})${tierLabel}`; + // Tier badge: clarify that "paid" means API tier, not model pricing + const tierBadge = model.tier === 'paid' ? color(' [Paid API]', 'warning') : ''; + return `${model.name}${tierBadge}`; +} + +/** + * Format model entry for detailed display (with description) + */ +function formatModelDetailed(model: ModelEntry, isCurrent: boolean): string { + const marker = isCurrent ? color('>', 'success') : ' '; + const name = isCurrent ? bold(model.name) : model.name; + const tierBadge = model.tier === 'paid' ? color(' [Paid API]', 'warning') : ''; + const desc = model.description ? dim(` - ${model.description}`) : ''; + return ` ${marker} ${name}${tierBadge}${desc}`; } /** @@ -72,6 +85,9 @@ export async function configureProviderModel( return false; } + // Initialize UI for colors/gradient + await initUI(); + // Build options list const options = catalog.models.map((m) => ({ id: m.id, @@ -82,9 +98,13 @@ export async function configureProviderModel( const defaultIdx = catalog.models.findIndex((m) => m.id === catalog.defaultModel); const safeDefaultIdx = defaultIdx >= 0 ? defaultIdx : 0; - // Show header + // Show header with context (gradient like ccs doctor) + console.error(''); + console.error(header(`Configure ${catalog.displayName} Model`)); + console.error(''); + console.error(dim(' Select which model to use for this provider.')); + console.error(dim(' Models marked [Paid API] require a paid Google AI Studio API key.')); console.error(''); - console.error(`[i] Configure ${catalog.displayName} model`); // Interactive selection const selectedModel = await InteractivePrompt.selectFromList('Select model:', options, { @@ -118,8 +138,8 @@ export async function configureProviderModel( const displayName = selectedEntry?.name || selectedModel; console.error(''); - console.error(`[OK] Model set to: ${displayName} (${selectedModel})`); - console.error(`[i] Saved to: ${settingsPath}`); + console.error(ok(`Model set to: ${bold(displayName)}`)); + console.error(dim(` Config saved: ${settingsPath}`)); console.error(''); return true; @@ -128,41 +148,47 @@ export async function configureProviderModel( /** * Show current model configuration */ -export function showCurrentConfig(provider: CLIProxyProvider): void { +export async function showCurrentConfig(provider: CLIProxyProvider): Promise { if (!supportsModelConfig(provider)) { - console.error(`[i] Provider ${provider} does not support model configuration`); + console.error(info(`Provider ${provider} does not support model configuration`)); return; } const catalog = getProviderCatalog(provider); if (!catalog) return; + // Initialize UI for colors/gradient + await initUI(); + const currentModel = getCurrentModel(provider); const settingsPath = getProviderSettingsPath(provider); console.error(''); - console.error(`[i] ${catalog.displayName} Model Configuration`); + console.error(header(`${catalog.displayName} Model Configuration`)); console.error(''); if (currentModel) { const entry = catalog.models.find((m) => m.id === currentModel); const displayName = entry?.name || 'Unknown'; - console.error(` Current: ${displayName} (${currentModel})`); - console.error(` Config: ${settingsPath}`); + console.error( + ` ${bold('Current:')} ${color(displayName, 'success')} ${dim(`(${currentModel})`)}` + ); + console.error(` ${bold('Config:')} ${dim(settingsPath)}`); } else { - console.error(` Current: (using defaults)`); - console.error(` Default: ${catalog.defaultModel}`); + console.error(` ${bold('Current:')} ${dim('(using defaults)')}`); + console.error(` ${bold('Default:')} ${catalog.defaultModel}`); } console.error(''); - console.error('Available models:'); + console.error(bold('Available models:')); + console.error(dim(' [Paid API] = Requires paid Google AI Studio API key')); + console.error(''); catalog.models.forEach((m) => { const isCurrent = m.id === currentModel; - const marker = isCurrent ? '>' : ' '; - console.error(` ${marker} ${formatModelOption(m)}`); + console.error(formatModelDetailed(m, isCurrent)); }); console.error(''); - console.error(`Run "ccs ${provider} --config" to change`); + console.error(dim(`Run "ccs ${provider} --config" to change`)); console.error(''); } diff --git a/tests/unit/cliproxy/model-config.test.js b/tests/unit/cliproxy/model-config.test.js index 24118bd7..807dad9b 100644 --- a/tests/unit/cliproxy/model-config.test.js +++ b/tests/unit/cliproxy/model-config.test.js @@ -55,20 +55,20 @@ describe('Model Config', () => { }); describe('showCurrentConfig', () => { - it('does not throw for agy provider', () => { + it('does not throw for agy provider', async () => { const { showCurrentConfig } = modelConfig; - // Just verify it doesn't throw - assert.doesNotThrow(() => showCurrentConfig('agy')); + // Just verify it doesn't throw (now async) + await assert.doesNotReject(async () => showCurrentConfig('agy')); }); - it('does not throw for gemini provider', () => { + it('does not throw for gemini provider', async () => { const { showCurrentConfig } = modelConfig; - assert.doesNotThrow(() => showCurrentConfig('gemini')); + await assert.doesNotReject(async () => showCurrentConfig('gemini')); }); - it('does not throw for unsupported provider', () => { + it('does not throw for unsupported provider', async () => { const { showCurrentConfig } = modelConfig; - assert.doesNotThrow(() => showCurrentConfig('codex')); + await assert.doesNotReject(async () => showCurrentConfig('codex')); }); });