From 8c0d75a454c923f8a592a5abbf82c03763c295bc Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 17 Mar 2026 14:35:27 -0400 Subject: [PATCH 1/6] fix(ui): rebalance API profile provider chooser --- .../profiles/profile-create-dialog.tsx | 147 ++++++++++-------- .../profiles/profile-create-dialog.test.tsx | 13 +- 2 files changed, 94 insertions(+), 66 deletions(-) diff --git a/ui/src/components/profiles/profile-create-dialog.tsx b/ui/src/components/profiles/profile-create-dialog.tsx index d08f0c98..c95dfc00 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'; @@ -125,6 +135,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 +198,7 @@ export function ProfileCreateDialog({ setActiveTab('basic'); setUrlWarning(null); setShowApiKey(false); + setShowMorePresets(false); setModelSearch(''); // Set initial preset based on initialMode @@ -215,11 +227,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 +298,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,28 +317,28 @@ 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)} /> ))} +
+ handlePresetSelect(CUSTOM_PRESET_ID)} + />
- - handlePresetSelect(CUSTOM_PRESET_ID)} - /> -
+ - {showQuickTemplates && ( -
- + {showQuickTemplates && (
-
+
{QUICK_TEMPLATE_PRESETS.map((preset) => (
-
- )} + )} +
@@ -683,41 +706,37 @@ function CompactPresetCard({ type="button" onClick={onClick} className={cn( - 'flex min-h-[104px] w-[180px] flex-none flex-col items-start gap-3 rounded-xl border px-3 py-3 text-left transition-all', + 'flex h-[78px] w-[168px] flex-none items-start gap-3 rounded-xl border px-3 py-2.5 text-left transition-all', isSelected ? 'border-primary bg-primary/8 shadow-sm ring-1 ring-primary/10' : 'border-border/60 bg-background hover:border-primary/40 hover:bg-accent/20' )} > -
+
{isAnthropicDirect ? ( - + ) : preset.icon ? ( -
- -
+ ) : ( -
+
{preset.name.charAt(0)}
)} - {preset.badge && ( - - {preset.badge} - - )}
-
-
{preset.name}
-

+

+
+
{preset.name}
+ {preset.badge && ( + + {preset.badge} + + )} +
+

{preset.description}

@@ -733,18 +752,20 @@ 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-[78px] w-[188px] flex-none items-start 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.customEndpoint')} +
+

{t('profileEditor.customEndpointDescription')}

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..6633e771 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(); + 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 +56,7 @@ describe('ProfileCreateDialog', () => { } await userEvent.click(glmButton); + expect(morePresetsToggle).toHaveAttribute('aria-expanded', 'true'); await waitFor(() => { expect(customButton).not.toHaveClass('border-primary'); }); From 098416aefd7659b9afbd212f864b3443dbb4e265 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 17 Mar 2026 14:35:34 -0400 Subject: [PATCH 2/6] docs(roadmap): record issue 748 chooser follow-up --- docs/project-roadmap.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/project-roadmap.md b/docs/project-roadmap.md index ec4bc5aa..15b1f88f 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 and shrinking chooser cards so the form fields stay visually primary. - **#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. From 93d94b597f94b34c1a89479bb94e5b8b08c440cc Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 17 Mar 2026 14:41:03 -0400 Subject: [PATCH 3/6] fix(ui): add llama.cpp provider icon --- src/shared/provider-preset-catalog.ts | 1 + tests/unit/api/provider-presets.test.ts | 1 + ui/public/assets/providers/llama-cpp.svg | 5 +++++ ui/tests/unit/ui/lib/provider-presets.test.ts | 4 ++++ 4 files changed, 11 insertions(+) create mode 100644 ui/public/assets/providers/llama-cpp.svg 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/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'); From ea15e5eb50528fa241e76e34630838c1e3e049a1 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 17 Mar 2026 14:41:09 -0400 Subject: [PATCH 4/6] docs(roadmap): mention llama.cpp icon polish --- docs/project-roadmap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/project-roadmap.md b/docs/project-roadmap.md index 15b1f88f..43a87d7c 100644 --- a/docs/project-roadmap.md +++ b/docs/project-roadmap.md @@ -41,7 +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 and shrinking chooser cards so the form fields stay visually primary. +- **#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. From 3954e445f331ee8e269c5bb3f74af7d8c196d322 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 17 Mar 2026 14:43:46 -0400 Subject: [PATCH 5/6] fix(ui): keep provider names readable --- ui/src/components/profiles/profile-create-dialog.tsx | 11 ++++++++--- .../profiles/profile-create-dialog.test.tsx | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ui/src/components/profiles/profile-create-dialog.tsx b/ui/src/components/profiles/profile-create-dialog.tsx index c95dfc00..00f68aa7 100644 --- a/ui/src/components/profiles/profile-create-dialog.tsx +++ b/ui/src/components/profiles/profile-create-dialog.tsx @@ -725,12 +725,17 @@ function CompactPresetCard({
-
-
{preset.name}
+
+
+ {preset.name} +
{preset.badge && ( {preset.badge} 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 6633e771..fb1f276b 100644 --- a/ui/tests/unit/components/profiles/profile-create-dialog.test.tsx +++ b/ui/tests/unit/components/profiles/profile-create-dialog.test.tsx @@ -34,6 +34,7 @@ describe('ProfileCreateDialog', () => { ); expect(screen.getByText('Featured Providers')).toBeInTheDocument(); + expect(screen.getByText('Alibaba Coding Plan')).not.toHaveClass('truncate'); const morePresetsToggle = screen.getByRole('button', { name: /More Presets/i }); expect(morePresetsToggle).toHaveAttribute('aria-expanded', 'false'); expect(document.body.querySelectorAll('.overflow-x-auto')).toHaveLength(1); From a999adc776029adebc3b67b16dfea2046bb9f73a Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Tue, 17 Mar 2026 14:51:07 -0400 Subject: [PATCH 6/6] fix(ui): simplify provider chooser cards --- .../profiles/profile-create-dialog.tsx | 39 +++++++++---------- .../profiles/profile-create-dialog.test.tsx | 2 +- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/ui/src/components/profiles/profile-create-dialog.tsx b/ui/src/components/profiles/profile-create-dialog.tsx index 00f68aa7..d36505e5 100644 --- a/ui/src/components/profiles/profile-create-dialog.tsx +++ b/ui/src/components/profiles/profile-create-dialog.tsx @@ -116,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, @@ -345,6 +339,7 @@ export function ProfileCreateDialog({ preset={preset} isSelected={selectedPreset === preset.id} onClick={() => handlePresetSelect(preset.id)} + density="featured" /> ))}
@@ -383,6 +378,7 @@ export function ProfileCreateDialog({ preset={preset} isSelected={selectedPreset === preset.id} onClick={() => handlePresetSelect(preset.id)} + density="compact" /> ))}
@@ -694,19 +690,23 @@ 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 ( ); @@ -757,7 +759,7 @@ function CustomPresetCard({ isSelected, onClick }: { isSelected: boolean; onClic type="button" onClick={onClick} className={cn( - 'flex h-[78px] w-[188px] flex-none items-start gap-3 rounded-xl border border-dashed px-3 py-2.5 text-left transition-all', + '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' @@ -766,13 +768,10 @@ function CustomPresetCard({ isSelected, onClick }: { isSelected: boolean; onClic
-
+
{t('profileEditor.customEndpoint')}
-

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

); 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 fb1f276b..df222572 100644 --- a/ui/tests/unit/components/profiles/profile-create-dialog.test.tsx +++ b/ui/tests/unit/components/profiles/profile-create-dialog.test.tsx @@ -34,7 +34,7 @@ describe('ProfileCreateDialog', () => { ); expect(screen.getByText('Featured Providers')).toBeInTheDocument(); - expect(screen.getByText('Alibaba Coding Plan')).not.toHaveClass('truncate'); + 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);