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' + ) })