From d7971ff03bb5061bf51bdaf6ea7ffdaa9b822c41 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Thu, 17 Sep 2026 16:34:08 +0300 Subject: [PATCH] fix(ai): reach the custom model option and update stale chat specs (#719) * fix(ai): keep the custom model option reachable The model picker appends its custom entry last, but the combobox renders only the first hundred rows until the user types. Providers with large catalogs, such as OpenRouter, pushed the entry past that cap, so a custom model ID was only discoverable by guessing that a search for "custom" would reveal it. Place the custom entry after the curated recommendations, ahead of the long tail, and give its group a label without the ellipsis that belongs to the option text. * fix(ui): stop pinning combobox group labels Pinned labels sit inside the scroll container's padding, so a scrolled row stayed visible as a clipped sliver above the label. Group labels now scroll with their rows. * test(chat): update specs for the current settings UI The credential persistence assertions pointed at test ids that moved to General settings and are covered by the credentials spec, the model editor returns through Cancel rather than Back, and the API-key link is an anchor whose href can be asserted directly instead of stubbing window.open. * docs: note the custom model picker fix * test(chat): assert the provider key page URL --- CHANGELOG.md | 1 + packages/vue/src/i18n/locales/de/ai.json | 1 + packages/vue/src/i18n/locales/es/ai.json | 1 + packages/vue/src/i18n/locales/fr/ai.json | 1 + packages/vue/src/i18n/locales/it/ai.json | 1 + packages/vue/src/i18n/locales/ja/ai.json | 1 + packages/vue/src/i18n/locales/pl/ai.json | 1 + packages/vue/src/i18n/locales/ru/ai.json | 1 + packages/vue/src/i18n/locales/zh-cn/ai.json | 1 + packages/vue/src/i18n/messages/ai.ts | 1 + src/app/ai/models/picker/options.ts | 1 + .../settings/profile-editor/selection.ts | 22 ++++++++++------- src/theme/select/combobox.ts | 3 +-- tests/e2e/chat/navigation.spec.ts | 7 ------ tests/e2e/chat/profiles.spec.ts | 24 ++++--------------- 15 files changed, 30 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9acbe801b..729742124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Mark unsaved documents and ask whether to save before closing a tab, the desktop window, or the application, rather than relying on recovery alone. - Defer AI provider connections and system credential reads until you send a message or use a connected feature, so opening documents and browsing chat history no longer trigger unexpected credential prompts. - Point Homebrew installation instructions to the official `openpencil` cask and document separate CLI installation. +- Reach the custom model option in the model picker for providers with large model catalogs instead of requiring a search for it. ## 0.15.0 — 2026-09-16 diff --git a/packages/vue/src/i18n/locales/de/ai.json b/packages/vue/src/i18n/locales/de/ai.json index 36b2435fa..2751dbe2f 100644 --- a/packages/vue/src/i18n/locales/de/ai.json +++ b/packages/vue/src/i18n/locales/de/ai.json @@ -87,6 +87,7 @@ "modelID": "Modell-ID", "customModelID": "Benutzerdefinierte Modell-ID", "customModel": "Benutzerdefiniertes Modell…", + "customModelGroup": "Eigenes Modell", "advancedModelSettings": "Erweiterte Einstellungen", "outputLimit": "Ausgabelimit", "outputLimitAutomatic": "Automatische Empfehlung", diff --git a/packages/vue/src/i18n/locales/es/ai.json b/packages/vue/src/i18n/locales/es/ai.json index 4021e316d..1fe1d61ed 100644 --- a/packages/vue/src/i18n/locales/es/ai.json +++ b/packages/vue/src/i18n/locales/es/ai.json @@ -93,6 +93,7 @@ "latest": "Nuevo", "customModelID": "ID de modelo personalizado", "customModel": "Modelo personalizado…", + "customModelGroup": "Modelo personalizado", "advancedModelSettings": "Ajustes avanzados", "outputLimit": "Límite de salida", "outputLimitAutomatic": "Recomendación automática", diff --git a/packages/vue/src/i18n/locales/fr/ai.json b/packages/vue/src/i18n/locales/fr/ai.json index 79b7029f4..4aadac145 100644 --- a/packages/vue/src/i18n/locales/fr/ai.json +++ b/packages/vue/src/i18n/locales/fr/ai.json @@ -87,6 +87,7 @@ "modelID": "ID du modèle", "customModelID": "ID du modèle personnalisé", "customModel": "Modèle personnalisé…", + "customModelGroup": "Modèle personnalisé", "advancedModelSettings": "Paramètres avancés", "outputLimit": "Limite de sortie", "outputLimitAutomatic": "Recommandation automatique", diff --git a/packages/vue/src/i18n/locales/it/ai.json b/packages/vue/src/i18n/locales/it/ai.json index 09d652e00..8b5c7e1c5 100644 --- a/packages/vue/src/i18n/locales/it/ai.json +++ b/packages/vue/src/i18n/locales/it/ai.json @@ -93,6 +93,7 @@ "latest": "Nuovo", "customModelID": "ID modello personalizzato", "customModel": "Modello personalizzato…", + "customModelGroup": "Modello personalizzato", "advancedModelSettings": "Impostazioni avanzate", "outputLimit": "Limite di output", "outputLimitAutomatic": "Raccomandazione automatica", diff --git a/packages/vue/src/i18n/locales/ja/ai.json b/packages/vue/src/i18n/locales/ja/ai.json index d328684d2..536a76cc4 100644 --- a/packages/vue/src/i18n/locales/ja/ai.json +++ b/packages/vue/src/i18n/locales/ja/ai.json @@ -93,6 +93,7 @@ "latest": "新着", "customModelID": "カスタムモデル ID", "customModel": "カスタムモデル…", + "customModelGroup": "カスタムモデル", "advancedModelSettings": "詳細設定", "outputLimit": "出力上限", "outputLimitAutomatic": "自動推奨値", diff --git a/packages/vue/src/i18n/locales/pl/ai.json b/packages/vue/src/i18n/locales/pl/ai.json index fd071dcb2..28e055944 100644 --- a/packages/vue/src/i18n/locales/pl/ai.json +++ b/packages/vue/src/i18n/locales/pl/ai.json @@ -87,6 +87,7 @@ "modelID": "ID modelu", "customModelID": "Identyfikator modelu niestandardowego", "customModel": "Model niestandardowy…", + "customModelGroup": "Własny model", "advancedModelSettings": "Ustawienia zaawansowane", "outputLimit": "Limit wyjścia", "outputLimitAutomatic": "Automatyczna rekomendacja", diff --git a/packages/vue/src/i18n/locales/ru/ai.json b/packages/vue/src/i18n/locales/ru/ai.json index 3f26aa69c..5c039a86d 100644 --- a/packages/vue/src/i18n/locales/ru/ai.json +++ b/packages/vue/src/i18n/locales/ru/ai.json @@ -87,6 +87,7 @@ "modelID": "ID модели", "customModelID": "ID пользовательской модели", "customModel": "Пользовательская модель…", + "customModelGroup": "Своя модель", "advancedModelSettings": "Расширенные настройки", "outputLimit": "Лимит вывода", "outputLimitAutomatic": "Автоматическая рекомендация", diff --git a/packages/vue/src/i18n/locales/zh-cn/ai.json b/packages/vue/src/i18n/locales/zh-cn/ai.json index fb1305481..6779ead82 100644 --- a/packages/vue/src/i18n/locales/zh-cn/ai.json +++ b/packages/vue/src/i18n/locales/zh-cn/ai.json @@ -93,6 +93,7 @@ "latest": "新", "customModelID": "自定义模型 ID", "customModel": "自定义模型…", + "customModelGroup": "自定义模型", "advancedModelSettings": "高级设置", "outputLimit": "输出限制", "outputLimitAutomatic": "自动建议", diff --git a/packages/vue/src/i18n/messages/ai.ts b/packages/vue/src/i18n/messages/ai.ts index 0505fd91b..aba5bccd2 100644 --- a/packages/vue/src/i18n/messages/ai.ts +++ b/packages/vue/src/i18n/messages/ai.ts @@ -135,6 +135,7 @@ export const aiMessageDefaults = { latest: 'Latest', customModelID: 'Custom model ID', customModel: 'Custom model…', + customModelGroup: 'Custom model', advancedModelSettings: 'Advanced settings', outputLimit: 'Output limit', outputLimitAutomatic: 'Automatic recommendation', diff --git a/src/app/ai/models/picker/options.ts b/src/app/ai/models/picker/options.ts index 172a763aa..e4a4fb9db 100644 --- a/src/app/ai/models/picker/options.ts +++ b/src/app/ai/models/picker/options.ts @@ -5,6 +5,7 @@ export interface ModelPickerLabels { latestModels: string allModels: string latest: string + customModelGroup: string } const LATEST_MODEL_COUNT = 8 diff --git a/src/app/ai/models/settings/profile-editor/selection.ts b/src/app/ai/models/settings/profile-editor/selection.ts index a4f962fe8..9d44e5925 100644 --- a/src/app/ai/models/settings/profile-editor/selection.ts +++ b/src/app/ai/models/settings/profile-editor/selection.ts @@ -42,16 +42,20 @@ export function useProfileModelSelection( }) const modelOptions = computed(() => { const options = modelPickerOptions(availableModels.value, providerDef.value.models, ai.value) - if (providerDef.value.supportsCustomModel) { - options.push({ - value: CUSTOM_MODEL_VALUE, - label: ai.value.customModel, - description: '', - meta: undefined, - group: ai.value.customModel - }) + if (!providerDef.value.supportsCustomModel) return options + // Keep the custom entry after the curated picks and ahead of the long tail, so it stays + // reachable in the picker's capped result list. + const customOption = { + value: CUSTOM_MODEL_VALUE, + label: ai.value.customModel, + description: '', + meta: undefined, + group: ai.value.customModelGroup } - return options + const group = ai.value.recommendedModels + const insertAt = options.findIndex((option) => option.group !== group) + if (insertAt === -1) return [...options, customOption] + return [...options.slice(0, insertAt), customOption, ...options.slice(insertAt)] }) const selectedModelValue = computed(() => customModelSelected.value ? CUSTOM_MODEL_VALUE : draft.modelID diff --git a/src/theme/select/combobox.ts b/src/theme/select/combobox.ts index 27fb503e8..94f47e9e9 100644 --- a/src/theme/select/combobox.ts +++ b/src/theme/select/combobox.ts @@ -16,8 +16,7 @@ const appComboboxTheme = { searchIcon: 'pointer-events-none absolute left-2 size-3 text-muted', viewport: 'max-h-64 overflow-y-auto p-0.5', empty: 'px-2 py-4 text-center text-[11px] text-muted', - groupLabel: - 'sticky top-0 z-10 bg-panel px-2 py-1 text-[9px] font-medium uppercase tracking-wide text-muted', + groupLabel: 'bg-panel px-2 py-1 text-[9px] font-medium uppercase tracking-wide text-muted', item: 'relative flex min-h-8 cursor-pointer items-center gap-2 rounded py-1 pr-2 pl-6 text-surface outline-none select-none data-[disabled]:pointer-events-none data-[highlighted]:bg-hover data-[disabled]:opacity-50', indicator: 'absolute left-1.5 inline-flex items-center justify-center', description: 'truncate font-mono text-[9px] text-muted', diff --git a/tests/e2e/chat/navigation.spec.ts b/tests/e2e/chat/navigation.spec.ts index 18a93f13b..fa00d2e0a 100644 --- a/tests/e2e/chat/navigation.spec.ts +++ b/tests/e2e/chat/navigation.spec.ts @@ -19,13 +19,6 @@ test('saving an API key in Settings opens the chat interface', async ({ chat }) await chat.chatTab.click() await chat.page.getByTestId('provider-setup-open-settings').click() await expect(chat.page.getByTestId('app-settings-dialog')).toBeVisible() - await expect(chat.page.getByTestId('settings-remember-credentials')).toHaveAttribute( - 'data-state', - 'checked' - ) - await expect(chat.page.getByTestId('settings-credential-backend')).toContainText( - 'encrypted browser storage' - ) await chat.page.locator('[data-model-id]').first().click() await chat.page.getByTestId('settings-model-provider').click() await chat.page.getByRole('option', { name: 'OpenRouter' }).click() diff --git a/tests/e2e/chat/profiles.spec.ts b/tests/e2e/chat/profiles.spec.ts index 839b0b551..4bb61979e 100644 --- a/tests/e2e/chat/profiles.spec.ts +++ b/tests/e2e/chat/profiles.spec.ts @@ -25,26 +25,12 @@ test('OpenRouter accepts a custom model ID from Settings', async ({ configuredCh await expect(chat.profileTrigger).toContainText('Claude Sonnet') }) -test('Get API key opens the provider URL', async ({ configuredChat: chat }) => { +test('Get API key links to the provider key page', async ({ configuredChat: chat }) => { await chat.page.getByTestId('provider-settings-trigger').click() await chat.page.locator('[data-model-id]').first().click() - await chat.page.getByTestId('provider-settings-clear-key').click() - await chat.page.getByRole('button', { name: 'Back' }).click() - await chat.page.getByTestId('app-settings-done').click() - await chat.page.getByTestId('provider-setup-open-settings').click() - await chat.page.locator('[data-model-id]').first().click() - await chat.page.getByTestId('settings-model-provider').click() - await chat.page.getByRole('option', { name: 'OpenRouter' }).click() - const openedURLs: string[] = [] - await chat.page.exposeFunction('mockWindowOpen', (url: string) => openedURLs.push(url)) - await chat.page.evaluate(() => { - window.open = (url: string | URL) => { - window.mockWindowOpen?.(String(url)) - return null - } - }) - await chat.page.getByRole('button', { name: 'Get API key →' }).click() - - await expect(() => expect(openedURLs[0]).toMatch(/^https:\/\//)).toPass() + await expect(chat.page.getByRole('link', { name: 'Get API key' })).toHaveAttribute( + 'href', + 'https://openrouter.ai/keys' + ) })