diff --git a/docs/project-roadmap.md b/docs/project-roadmap.md index ec4bc5aa..43a87d7c 100644 --- a/docs/project-roadmap.md +++ b/docs/project-roadmap.md @@ -41,6 +41,7 @@ All major modularization work is complete. The codebase evolved from monolithic ### Recent Fixes +- **#748**: API profile creation now keeps provider selection compact by collapsing advanced presets behind an explicit toggle, shrinking chooser cards so the form fields stay visually primary, and giving `llama.cpp` a dedicated provider logo. - **#744**: API profile creation now keeps featured providers in a horizontal rail with scroll fallback, moves Anthropic Direct API to the end, reuses the shared Claude logo, and separates the custom-endpoint entry point from advanced template discovery. - **#724**: Codex startup is now free-plan safe. CCS defaults new Codex sessions to a cross-plan model and auto-repairs stale paid-only Codex defaults when the active account is on the free plan. - **#737**: Dashboard model pickers in Cursor, Copilot, and CLIProxy now use a searchable combobox with autofocus and explicit no-results states for large model catalogs. diff --git a/src/shared/provider-preset-catalog.ts b/src/shared/provider-preset-catalog.ts index 42c6e7be..53c50678 100644 --- a/src/shared/provider-preset-catalog.ts +++ b/src/shared/provider-preset-catalog.ts @@ -118,6 +118,7 @@ const RAW_PROVIDER_PRESET_DEFINITIONS: readonly ProviderPresetDefinition[] = [ requiresApiKey: false, badge: 'Local', featured: true, + icon: '/assets/providers/llama-cpp.svg', }, { id: 'anthropic', diff --git a/tests/unit/api/provider-presets.test.ts b/tests/unit/api/provider-presets.test.ts index e4518e07..9a3323f9 100644 --- a/tests/unit/api/provider-presets.test.ts +++ b/tests/unit/api/provider-presets.test.ts @@ -35,6 +35,7 @@ describe('provider-presets', () => { expect(preset?.requiresApiKey).toBe(false); expect(preset?.apiKeyPlaceholder).toBe('llamacpp'); expect(preset?.baseUrl).toBe('http://127.0.0.1:8080'); + expect(preset?.icon).toBe('/assets/providers/llama-cpp.svg'); }); it('resolves legacy kimi preset alias to km', () => { diff --git a/ui/public/assets/providers/llama-cpp.svg b/ui/public/assets/providers/llama-cpp.svg new file mode 100644 index 00000000..9f2d0875 --- /dev/null +++ b/ui/public/assets/providers/llama-cpp.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ui/src/components/profiles/profile-create-dialog.tsx b/ui/src/components/profiles/profile-create-dialog.tsx index d08f0c98..d36505e5 100644 --- a/ui/src/components/profiles/profile-create-dialog.tsx +++ b/ui/src/components/profiles/profile-create-dialog.tsx @@ -31,7 +31,17 @@ import { Badge } from '@/components/ui/badge'; import { ProviderLogo } from '@/components/cliproxy/provider-logo'; import { useCreateProfile } from '@/hooks/use-profiles'; import { useOpenRouterCatalog } from '@/hooks/use-openrouter-models'; -import { Loader2, Plus, AlertTriangle, Info, Eye, EyeOff, Settings2, Sparkles } from 'lucide-react'; +import { + Loader2, + Plus, + AlertTriangle, + Info, + Eye, + EyeOff, + Settings2, + Sparkles, + ChevronDown, +} from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; import { cn } from '@/lib/utils'; @@ -106,12 +116,6 @@ const QUICK_TEMPLATE_PRESETS = PROVIDER_PRESETS.filter( const QUICK_TEMPLATE_PRESET_IDS = new Set( QUICK_TEMPLATE_PRESETS.map((preset) => preset.id) ); -const CARD_META_CLAMP_STYLE = { - display: '-webkit-box', - WebkitLineClamp: 2, - WebkitBoxOrient: 'vertical', - overflow: 'hidden', -} as const; export function ProfileCreateDialog({ open, @@ -125,6 +129,7 @@ export function ProfileCreateDialog({ const [urlWarning, setUrlWarning] = useState(null); const [showApiKey, setShowApiKey] = useState(false); const [selectedPreset, setSelectedPreset] = useState(DEFAULT_PRESET_ID); + const [showMorePresets, setShowMorePresets] = useState(false); const [modelSearch, setModelSearch] = useState(''); // OpenRouter models for model picker @@ -187,6 +192,7 @@ export function ProfileCreateDialog({ setActiveTab('basic'); setUrlWarning(null); setShowApiKey(false); + setShowMorePresets(false); setModelSearch(''); // Set initial preset based on initialMode @@ -215,11 +221,13 @@ export function ProfileCreateDialog({ if (preset) { setSelectedPreset(preset.id); + setShowMorePresets(QUICK_TEMPLATE_PRESET_IDS.has(preset.id)); applyPresetToForm(preset); return; } setSelectedPreset(CUSTOM_PRESET_ID); + setShowMorePresets(false); applyPresetToForm(null); }; @@ -284,7 +292,7 @@ export function ProfileCreateDialog({ const isQuickTemplateSelected = selectedPreset !== CUSTOM_PRESET_ID && QUICK_TEMPLATE_PRESET_IDS.has(selectedPreset); const isOpenRouter = currentPreset?.id === DEFAULT_PRESET_ID; - const showQuickTemplates = selectedPreset === CUSTOM_PRESET_ID || isQuickTemplateSelected; + const showQuickTemplates = showMorePresets || isQuickTemplateSelected; return ( @@ -303,69 +311,80 @@ export function ProfileCreateDialog({ onSubmit={handleSubmit(onSubmit)} className="flex flex-col flex-1 min-h-0 overflow-hidden" > -
-
-
- -

- {t('profileEditor.providerChooserHint')} -

+
+
+
+
+ +

+ {t('profileEditor.providerChooserHint')} +

+
+ + {t('profileEditor.scrollHint')} +
-
- - - {t('profileEditor.scrollHint')} - -
+
-
+
{RECOMMENDED_PRESETS.map((preset) => ( handlePresetSelect(preset.id)} + density="featured" /> ))} +
+ handlePresetSelect(CUSTOM_PRESET_ID)} + />
- - handlePresetSelect(CUSTOM_PRESET_ID)} - /> -
+ - {showQuickTemplates && ( -
- + {showQuickTemplates && (
-
+
{QUICK_TEMPLATE_PRESETS.map((preset) => ( handlePresetSelect(preset.id)} + density="compact" /> ))}
-
- )} + )} +
@@ -671,56 +690,63 @@ function CompactPresetCard({ preset, isSelected, onClick, + density = 'compact', }: { preset: ProviderPreset; isSelected: boolean; onClick: () => void; + density?: 'featured' | 'compact'; }) { const isAnthropicDirect = preset.id === 'anthropic'; + const isFeaturedDensity = density === 'featured'; return ( ); } @@ -733,20 +759,19 @@ function CustomPresetCard({ isSelected, onClick }: { isSelected: boolean; onClic type="button" onClick={onClick} className={cn( - 'flex w-full items-center gap-3 rounded-xl border border-dashed px-4 py-3 text-left transition-all sm:max-w-[280px]', + 'flex h-[68px] w-[236px] flex-none items-center gap-3 rounded-xl border border-dashed px-3 py-2.5 text-left transition-all', isSelected ? 'border-primary bg-primary/8 shadow-sm ring-1 ring-primary/10' : 'border-muted-foreground/30 bg-background hover:border-primary/40 hover:bg-accent/20' )} > -
+
-
-
{t('profileEditor.customEndpoint')}
-

- {t('profileEditor.customEndpointDescription')} -

+
+
+ {t('profileEditor.customEndpoint')} +
); diff --git a/ui/tests/unit/components/profiles/profile-create-dialog.test.tsx b/ui/tests/unit/components/profiles/profile-create-dialog.test.tsx index 5ba4da8e..df222572 100644 --- a/ui/tests/unit/components/profiles/profile-create-dialog.test.tsx +++ b/ui/tests/unit/components/profiles/profile-create-dialog.test.tsx @@ -23,7 +23,7 @@ describe('ProfileCreateDialog', () => { mutateAsync.mockReset(); }); - it('reveals more presets from custom mode and keeps custom unselected after choosing a template', async () => { + it('keeps advanced presets collapsed until explicitly opened and deselects custom after choosing a template', async () => { render( { ); expect(screen.getByText('Featured Providers')).toBeInTheDocument(); - expect(screen.queryByText('More Presets')).not.toBeInTheDocument(); + expect(screen.getByText('Alibaba Coding Plan')).toBeVisible(); + const morePresetsToggle = screen.getByRole('button', { name: /More Presets/i }); + expect(morePresetsToggle).toHaveAttribute('aria-expanded', 'false'); expect(document.body.querySelectorAll('.overflow-x-auto')).toHaveLength(1); const customButton = screen.getByRole('button', { name: /Custom Endpoint/i }); await userEvent.click(customButton); - expect(await screen.findByText('More Presets')).toBeInTheDocument(); + expect(morePresetsToggle).toHaveAttribute('aria-expanded', 'false'); + expect(document.body.querySelectorAll('.overflow-x-auto')).toHaveLength(1); + + await userEvent.click(morePresetsToggle); + + expect(morePresetsToggle).toHaveAttribute('aria-expanded', 'true'); expect(document.body.querySelectorAll('.overflow-x-auto')).toHaveLength(2); const glmButton = screen.getByText('GLM').closest('button'); @@ -50,6 +57,7 @@ describe('ProfileCreateDialog', () => { } await userEvent.click(glmButton); + expect(morePresetsToggle).toHaveAttribute('aria-expanded', 'true'); await waitFor(() => { expect(customButton).not.toHaveClass('border-primary'); }); diff --git a/ui/tests/unit/ui/lib/provider-presets.test.ts b/ui/tests/unit/ui/lib/provider-presets.test.ts index a8566369..53ab8c65 100644 --- a/ui/tests/unit/ui/lib/provider-presets.test.ts +++ b/ui/tests/unit/ui/lib/provider-presets.test.ts @@ -28,6 +28,10 @@ describe('resolvePresetApiKeyValue', () => { }); describe('provider preset metadata', () => { + it('uses the llama.cpp provider logo asset for the local llama.cpp preset', () => { + expect(getPresetById('llamacpp')?.icon).toBe('/assets/providers/llama-cpp.svg'); + }); + it('keeps Anthropic direct last in the recommended order', () => { const recommendedPresetIds = getPresetsByCategory('recommended').map((preset) => preset.id); expect(recommendedPresetIds.at(-1)).toBe('anthropic');