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
This commit is contained in:
Danila Poyarkov 2026-09-17 16:34:08 +03:00 committed by GitHub
parent 9d2b97e679
commit d7971ff03b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 30 additions and 37 deletions

View file

@ -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

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -93,6 +93,7 @@
"latest": "新着",
"customModelID": "カスタムモデル ID",
"customModel": "カスタムモデル…",
"customModelGroup": "カスタムモデル",
"advancedModelSettings": "詳細設定",
"outputLimit": "出力上限",
"outputLimitAutomatic": "自動推奨値",

View file

@ -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",

View file

@ -87,6 +87,7 @@
"modelID": "ID модели",
"customModelID": "ID пользовательской модели",
"customModel": "Пользовательская модель…",
"customModelGroup": "Своя модель",
"advancedModelSettings": "Расширенные настройки",
"outputLimit": "Лимит вывода",
"outputLimitAutomatic": "Автоматическая рекомендация",

View file

@ -93,6 +93,7 @@
"latest": "新",
"customModelID": "自定义模型 ID",
"customModel": "自定义模型…",
"customModelGroup": "自定义模型",
"advancedModelSettings": "高级设置",
"outputLimit": "输出限制",
"outputLimitAutomatic": "自动建议",

View file

@ -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',

View file

@ -5,6 +5,7 @@ export interface ModelPickerLabels {
latestModels: string
allModels: string
latest: string
customModelGroup: string
}
const LATEST_MODEL_COUNT = 8

View file

@ -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

View file

@ -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',

View file

@ -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()

View file

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