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:
parent
9d2b97e679
commit
d7971ff03b
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@
|
|||
"latest": "新着",
|
||||
"customModelID": "カスタムモデル ID",
|
||||
"customModel": "カスタムモデル…",
|
||||
"customModelGroup": "カスタムモデル",
|
||||
"advancedModelSettings": "詳細設定",
|
||||
"outputLimit": "出力上限",
|
||||
"outputLimitAutomatic": "自動推奨値",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@
|
|||
"modelID": "ID модели",
|
||||
"customModelID": "ID пользовательской модели",
|
||||
"customModel": "Пользовательская модель…",
|
||||
"customModelGroup": "Своя модель",
|
||||
"advancedModelSettings": "Расширенные настройки",
|
||||
"outputLimit": "Лимит вывода",
|
||||
"outputLimitAutomatic": "Автоматическая рекомендация",
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@
|
|||
"latest": "新",
|
||||
"customModelID": "自定义模型 ID",
|
||||
"customModel": "自定义模型…",
|
||||
"customModelGroup": "自定义模型",
|
||||
"advancedModelSettings": "高级设置",
|
||||
"outputLimit": "输出限制",
|
||||
"outputLimitAutomatic": "自动建议",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ export interface ModelPickerLabels {
|
|||
latestModels: string
|
||||
allModels: string
|
||||
latest: string
|
||||
customModelGroup: string
|
||||
}
|
||||
|
||||
const LATEST_MODEL_COUNT = 8
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
)
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue