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.
This commit is contained in:
kaitranntt committed 2025-12-04 05:29:55 -05:00
1 parent e3a18358e9
commit 20b7bae046
3 files changed
+86 -32

No files matched your search

+35 -7
View File
@@ -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<Record<CLIProxyProvider, ProviderCatalog>> =
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<Record<CLIProxyProvider, ProviderCatalog>> =
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',
},
],
},
};
+44 -18
View File
@@ -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<void> {
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('');
}
+7 -7
View File
@@ -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'));
});
});