diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index ab28dbe5d..beb5741c9 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -229,16 +229,37 @@ export const AI_PROVIDERS: AIProviderDef[] = [ defaultModel: 'anthropic/claude-sonnet-4.6', supportsCustomModel: true, models: [ - { id: 'anthropic/claude-sonnet-4.6', name: 'Claude Sonnet 4.6', tag: 'Best for design' }, - { id: 'anthropic/claude-opus-4.6', name: 'Claude Opus 4.6', tag: 'Smartest' }, - { id: 'moonshotai/kimi-k2.5', name: 'Kimi K2.5', tag: 'Vision + code' }, + { + id: 'anthropic/claude-sonnet-4.6', + name: 'Claude Sonnet 4.6', + tag: 'Best for design', + capabilities: ['tools', 'vision'] + }, + { + id: 'anthropic/claude-opus-4.6', + name: 'Claude Opus 4.6', + tag: 'Smartest', + capabilities: ['tools', 'vision'] + }, + { + id: 'moonshotai/kimi-k2.5', + name: 'Kimi K2.5', + tag: 'Vision + code', + capabilities: ['tools', 'vision'] + }, { id: 'google/gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro', - tag: '1M context' + tag: '1M context', + capabilities: ['tools', 'vision'] }, { id: 'openai/gpt-5.3-codex', name: 'GPT-5.3 Codex' }, - { id: 'google/gemini-3-flash-preview', name: 'Gemini 3 Flash', tag: 'Fast' }, + { + id: 'google/gemini-3-flash-preview', + name: 'Gemini 3 Flash', + tag: 'Fast', + capabilities: ['tools', 'vision'] + }, { id: 'deepseek/deepseek-v3.2', name: 'DeepSeek V3.2', tag: 'Cheap' }, { id: 'qwen/qwen3.5-flash-02-23', name: 'Qwen 3.5 Flash', tag: 'Cheap' }, { id: 'qwen/qwen3-coder:free', name: 'Qwen3 Coder', tag: 'Free' }, @@ -252,8 +273,18 @@ export const AI_PROVIDERS: AIProviderDef[] = [ keyURL: 'https://console.anthropic.com/settings/keys', defaultModel: 'claude-sonnet-4-6-20260301', models: [ - { id: 'claude-sonnet-4-6-20260301', name: 'Claude Sonnet 4.6', tag: 'Best for design' }, - { id: 'claude-opus-4-6-20260301', name: 'Claude Opus 4.6', tag: 'Smartest' } + { + id: 'claude-sonnet-4-6-20260301', + name: 'Claude Sonnet 4.6', + tag: 'Best for design', + capabilities: ['tools', 'vision'] + }, + { + id: 'claude-opus-4-6-20260301', + name: 'Claude Opus 4.6', + tag: 'Smartest', + capabilities: ['tools', 'vision'] + } ] }, { @@ -276,8 +307,18 @@ export const AI_PROVIDERS: AIProviderDef[] = [ keyURL: 'https://aistudio.google.com/apikey', defaultModel: 'gemini-3.1-pro-preview', models: [ - { id: 'gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro', tag: '1M context' }, - { id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash', tag: 'Fast' } + { + id: 'gemini-3.1-pro-preview', + name: 'Gemini 3.1 Pro', + tag: '1M context', + capabilities: ['tools', 'vision'] + }, + { + id: 'gemini-3-flash-preview', + name: 'Gemini 3 Flash', + tag: 'Fast', + capabilities: ['tools', 'vision'] + } ] }, { diff --git a/packages/vue/src/i18n/locales/de/ai.json b/packages/vue/src/i18n/locales/de/ai.json index 119f80625..a78de364c 100644 --- a/packages/vue/src/i18n/locales/de/ai.json +++ b/packages/vue/src/i18n/locales/de/ai.json @@ -91,6 +91,7 @@ "chatInsufficientCredit": "Das Anbieterkonto hat nicht genügend Guthaben oder Kontingent. Lade es auf und versuche es erneut.", "chatOutputLimit": "Das Modell hat beim Schlussfolgern das Ausgabetoken-Limit erreicht. Erhöhe die maximalen Ausgabetoken oder verringere den Schlussfolgerungsaufwand.", "chatRequestFailed": "Die Modellanfrage ist fehlgeschlagen. Prüfe die Anbietereinstellungen und versuche es erneut.", + "visionModelUnavailable": "Wähle in den Einstellungen ein Vision-Modell aus, bevor du Bilder anhängst.", "completions": "Completions", "responses": "Responses", "connectProvider": "Verbinde einen KI-Anbieter, um den Chat zu starten.", diff --git a/packages/vue/src/i18n/locales/es/ai.json b/packages/vue/src/i18n/locales/es/ai.json index eba5f0dec..62ded8c4b 100644 --- a/packages/vue/src/i18n/locales/es/ai.json +++ b/packages/vue/src/i18n/locales/es/ai.json @@ -91,6 +91,7 @@ "chatInsufficientCredit": "La cuenta del proveedor no tiene suficiente saldo o cuota. Recárgala e inténtalo de nuevo.", "chatOutputLimit": "El modelo agotó los tokens de salida mientras razonaba. Aumenta los tokens máximos de salida o reduce el esfuerzo de razonamiento.", "chatRequestFailed": "La solicitud al modelo falló. Comprueba la configuración del proveedor e inténtalo de nuevo.", + "visionModelUnavailable": "Elige un modelo de visión en Ajustes antes de adjuntar imágenes.", "completions": "Completions", "responses": "Responses", "connectProvider": "Conecta un proveedor de IA para empezar a chatear.", diff --git a/packages/vue/src/i18n/locales/fr/ai.json b/packages/vue/src/i18n/locales/fr/ai.json index 9eacdf07c..db94099b5 100644 --- a/packages/vue/src/i18n/locales/fr/ai.json +++ b/packages/vue/src/i18n/locales/fr/ai.json @@ -91,6 +91,7 @@ "chatInsufficientCredit": "Le compte du fournisseur n’a pas assez de crédit ou de quota. Rechargez-le et réessayez.", "chatOutputLimit": "Le modèle a épuisé ses jetons de sortie pendant le raisonnement. Augmentez le nombre maximal de jetons de sortie ou réduisez l’effort de raisonnement.", "chatRequestFailed": "La requête au modèle a échoué. Vérifiez les paramètres du fournisseur et réessayez.", + "visionModelUnavailable": "Choisissez un modèle Vision dans les paramètres avant de joindre des images.", "completions": "Completions", "responses": "Responses", "connectProvider": "Connectez un fournisseur d’IA pour commencer à discuter.", diff --git a/packages/vue/src/i18n/locales/it/ai.json b/packages/vue/src/i18n/locales/it/ai.json index aa262a977..e472b4393 100644 --- a/packages/vue/src/i18n/locales/it/ai.json +++ b/packages/vue/src/i18n/locales/it/ai.json @@ -91,6 +91,7 @@ "chatInsufficientCredit": "L’account del provider non dispone di credito o quota sufficiente. Ricaricalo e riprova.", "chatOutputLimit": "Il modello ha esaurito i token di output durante il ragionamento. Aumenta i token di output massimi o riduci lo sforzo di ragionamento.", "chatRequestFailed": "La richiesta al modello non è riuscita. Controlla le impostazioni del provider e riprova.", + "visionModelUnavailable": "Scegli un modello Vision nelle Impostazioni prima di allegare immagini.", "completions": "Completions", "responses": "Responses", "connectProvider": "Collega un provider AI per iniziare a chattare.", diff --git a/packages/vue/src/i18n/locales/ja/ai.json b/packages/vue/src/i18n/locales/ja/ai.json index b6f55b87b..264fd4bd8 100644 --- a/packages/vue/src/i18n/locales/ja/ai.json +++ b/packages/vue/src/i18n/locales/ja/ai.json @@ -91,6 +91,7 @@ "chatInsufficientCredit": "プロバイダーのアカウントに十分なクレジットまたは割り当てがありません。チャージして再試行してください。", "chatOutputLimit": "推論中にモデルの出力トークンが上限に達しました。最大出力トークンを増やすか、推論の強度を下げてください。", "chatRequestFailed": "モデルへのリクエストに失敗しました。プロバイダー設定を確認して再試行してください。", + "visionModelUnavailable": "画像を添付する前に、設定でビジョンモデルを選択してください。", "completions": "入力補完", "responses": "応答", "connectProvider": "チャットを始めるには、AIプロバイダーを接続してください。", diff --git a/packages/vue/src/i18n/locales/pl/ai.json b/packages/vue/src/i18n/locales/pl/ai.json index 60e5e1cc7..239d864bd 100644 --- a/packages/vue/src/i18n/locales/pl/ai.json +++ b/packages/vue/src/i18n/locales/pl/ai.json @@ -91,6 +91,7 @@ "chatInsufficientCredit": "Konto dostawcy nie ma wystarczających środków lub limitu. Doładuj je i spróbuj ponownie.", "chatOutputLimit": "Model wyczerpał limit tokenów wyjściowych podczas rozumowania. Zwiększ maksymalną liczbę tokenów wyjściowych lub zmniejsz poziom rozumowania.", "chatRequestFailed": "Żądanie do modelu nie powiodło się. Sprawdź ustawienia dostawcy i spróbuj ponownie.", + "visionModelUnavailable": "Przed dołączeniem obrazów wybierz model Vision w Ustawieniach.", "completions": "Completions", "responses": "Responses", "connectProvider": "Połącz dostawcę AI, aby rozpocząć czat.", diff --git a/packages/vue/src/i18n/locales/ru/ai.json b/packages/vue/src/i18n/locales/ru/ai.json index 39b82139b..958fb6a3b 100644 --- a/packages/vue/src/i18n/locales/ru/ai.json +++ b/packages/vue/src/i18n/locales/ru/ai.json @@ -91,6 +91,7 @@ "chatInsufficientCredit": "На счёте провайдера недостаточно средств или квоты. Пополните его и повторите попытку.", "chatOutputLimit": "Модель исчерпала лимит выходных токенов во время рассуждения. Увеличьте максимум выходных токенов или снизьте глубину рассуждения.", "chatRequestFailed": "Запрос к модели завершился ошибкой. Проверьте настройки провайдера и повторите попытку.", + "visionModelUnavailable": "Перед добавлением изображений выберите модель для зрения в настройках.", "completions": "Completions", "responses": "Responses", "connectProvider": "Подключите AI-провайдера, чтобы начать чат.", diff --git a/packages/vue/src/i18n/locales/zh-cn/ai.json b/packages/vue/src/i18n/locales/zh-cn/ai.json index 08f01d851..275a5dcb4 100644 --- a/packages/vue/src/i18n/locales/zh-cn/ai.json +++ b/packages/vue/src/i18n/locales/zh-cn/ai.json @@ -91,6 +91,7 @@ "chatInsufficientCredit": "提供商账户的余额或配额不足。请充值后重试。", "chatOutputLimit": "模型在推理时耗尽了输出 token。请提高最大输出 token 数或降低推理强度。", "chatRequestFailed": "模型请求失败。请检查提供商设置后重试。", + "visionModelUnavailable": "附加图片前,请在设置中选择视觉模型。", "completions": "Completions", "responses": "Responses", "connectProvider": "连接 AI 提供商以开始聊天。", diff --git a/packages/vue/src/i18n/messages/ai.ts b/packages/vue/src/i18n/messages/ai.ts index 97ccf938d..c2650ab29 100644 --- a/packages/vue/src/i18n/messages/ai.ts +++ b/packages/vue/src/i18n/messages/ai.ts @@ -106,6 +106,7 @@ export const aiMessageDefaults = { chatOutputLimit: 'The model ran out of output tokens while reasoning. Raise Max output tokens or lower reasoning effort.', chatRequestFailed: 'The model request failed. Check the provider settings and try again.', + visionModelUnavailable: 'Choose a Vision model in Settings before attaching images.', completions: 'Completions', responses: 'Responses' } as const diff --git a/src/app/ai/attachment/image/analyze.ts b/src/app/ai/attachment/image/analyze.ts index af6315bda..500d2e6a5 100644 --- a/src/app/ai/attachment/image/analyze.ts +++ b/src/app/ai/attachment/image/analyze.ts @@ -14,6 +14,13 @@ const MAX_IMAGE_ANALYSIS_TOKENS = 1200 export type ImageAnalysisDependencies = VisionModelDependencies +export class VisionModelUnavailableError extends Error { + constructor() { + super('Configure a Vision model in Settings to attach images.') + this.name = 'VisionModelUnavailableError' + } +} + export async function analyzeAttachedImages( store: EditorStore, instruction: string, @@ -24,9 +31,7 @@ export async function analyzeAttachedImages( } ): Promise { const runtime = await dependencies.createRuntime('vision') - if (runtime?.kind !== 'direct') { - throw new Error('Configure a vision-capable model in Settings to attach images.') - } + if (runtime?.kind !== 'direct') throw new VisionModelUnavailableError() const content: Array< | { type: 'text'; text: string } diff --git a/src/app/ai/models/store.ts b/src/app/ai/models/store.ts index 5e1ba4430..673b55e59 100644 --- a/src/app/ai/models/store.ts +++ b/src/app/ai/models/store.ts @@ -119,6 +119,29 @@ function optionalAssignment(value: unknown, modelIds: Set): AIModelRoleA return typeof value === 'string' && modelIds.has(value) ? (value as AIModelProfileId) : null } +function curatedModelCapabilities( + providerID: AIProviderID, + modelID: string +): AIModelCapability[] | null { + const model = AI_PROVIDERS.find((provider) => provider.id === providerID)?.models.find( + (candidate) => candidate.id === modelID + ) + return model?.capabilities ? [...model.capabilities] : null +} + +function hydrateCuratedCapabilities( + profiles: AIModelProfile[], + connections: AIModelConnection[] +): void { + for (const profile of profiles) { + if (profile.customModelID) continue + const connection = connections.find((candidate) => candidate.id === profile.connectionId) + if (!connection) continue + const capabilities = curatedModelCapabilities(connection.providerID, profile.modelID) + if (capabilities) profile.capabilities = [...new Set(capabilities)] + } +} + function parseSettings(value: unknown): AIModelSettings | null { if (!isRecord(value) || value.version !== 1) return null const connections = Array.isArray(value.connections) @@ -131,6 +154,7 @@ function parseSettings(value: unknown): AIModelSettings | null { .filter((profile) => profile !== null) : [] if (!models.length) return null + hydrateCuratedCapabilities(models, connections) const modelIds = new Set(models.map((profile) => profile.id)) const rawAssignments = isRecord(value.assignments) ? value.assignments : {} const rawDesign = stringValue(rawAssignments.design) @@ -167,6 +191,7 @@ function legacySettings(): AIModelSettings { ? provider?.models.find((model) => model.id === displayModel)?.name || displayModel : 'Design model' const maxOutputTokens = Number(readLegacyAIModelStorage('ai-max-output-tokens')) + const curatedCapabilities = customModelID ? null : curatedModelCapabilities(providerID, modelID) return { version: 1, connections: [ @@ -189,14 +214,14 @@ function legacySettings(): AIModelSettings { maxOutputTokens: Number.isFinite(maxOutputTokens) ? maxOutputTokens : DEFAULT_MAX_OUTPUT_TOKENS, - capabilities: ['tools'] + capabilities: curatedCapabilities ?? ['tools'] } ], assignments: { design: LEGACY_MODEL_ID, review: 'design', fast: 'design', - vision: null + vision: curatedCapabilities?.includes('vision') ? 'design' : null } } } @@ -372,6 +397,13 @@ export function saveModelProfileDraft(draft: AIModelProfileDraft): AIModelProfil const index = aiModelSettings.value.models.findIndex((model) => model.id === profile.id) if (index === -1) aiModelSettings.value.models.push(profile) else aiModelSettings.value.models[index] = profile + if ( + aiModelSettings.value.assignments.vision === null && + aiModelSettings.value.assignments.design === profile.id && + profile.capabilities.includes('vision') + ) { + aiModelSettings.value.assignments.vision = 'design' + } if ( aiModelSettings.value.assignments.vision === profile.id && !profile.capabilities.includes('vision') diff --git a/src/components/ChatPanel.vue b/src/components/ChatPanel.vue index 09cbc8506..b0bb54a8d 100644 --- a/src/components/ChatPanel.vue +++ b/src/components/ChatPanel.vue @@ -8,7 +8,8 @@ import { copyChatLog } from '@/app/ai/debug' import { clearVisibleMessageText, setVisibleMessageText } from '@/app/ai/chat/presentation' import { analyzeAttachedImages, - designMessageWithImageFindings + designMessageWithImageFindings, + VisionModelUnavailableError } from '@/app/ai/attachment/image/analyze' import { createImagePreviewURL, @@ -32,6 +33,7 @@ import AppButton from '@/components/ui/AppButton.vue' import ProviderSetup from '@/components/chat/ProviderSetup.vue' import { useAIChat } from '@/app/ai/chat/use' import { toast } from '@/app/shell/ui' +import { openSettingsDialog } from '@/app/settings/dialog' import { useI18n } from '@open-pencil/vue' import { useNotificationMessages } from '@/app/i18n/notifications' @@ -151,6 +153,16 @@ async function sendTextMessage( if (message) setVisibleMessageText(message.id, displayText) } +function reportSubmitError(error: unknown): void { + console.error('Chat error:', error) + if (error instanceof VisionModelUnavailableError) { + toast.error(ai.value.visionModelUnavailable) + openSettingsDialog('ai') + return + } + toast.error(ai.value.chatRequestFailed) +} + async function handleSubmit(text: string, images: ImageAttachmentDraft[] = [], displayText = text) { if (status.value === 'streaming' || status.value === 'submitted' || isPreparingImages.value) { for (const image of images) revokeImagePreviewURL(image.previewURL) @@ -229,9 +241,8 @@ async function handleSubmit(text: string, images: ImageAttachmentDraft[] = [], d findings ) }) - } catch (e) { - console.error('Chat error:', e) - toast.error(ai.value.chatRequestFailed) + } catch (error) { + reportSubmitError(error) } finally { if (operationVersion === attachmentOperationVersion) isPreparingImages.value = false } diff --git a/src/components/chat/ChatInput.vue b/src/components/chat/ChatInput.vue index b7e5eb720..766f12d76 100644 --- a/src/components/chat/ChatInput.vue +++ b/src/components/chat/ChatInput.vue @@ -7,7 +7,6 @@ import { useI18n, useSelectionState } from '@open-pencil/vue' import ChatNodePreview from '@/components/chat/ChatNodePreview.vue' import ChatProfileSelect from '@/components/chat/ChatProfileSelect.vue' -import ProviderModelSelect from '@/components/chat/ProviderModelSelect.vue' import IconButton from '@/components/ui/IconButton.vue' import InputGroup from '@/components/ui/InputGroup.vue' import { useAIChat } from '@/app/ai/chat/use' @@ -16,7 +15,7 @@ import { MAX_REFERENCED_NODES, resolveReferencedNodes } from '@/app/ai/chat/context' -import { designModelProfile, designModelProfiles } from '@/app/ai/models' +import { designModelProfile } from '@/app/ai/models' import { createImagePreviewURL, revokeImagePreviewURL, @@ -125,9 +124,7 @@ const selectedModelName = computed(() => { return providerDef.value.models.find((m) => m.id === modelID.value)?.name ?? modelID.value }) -// Switching between saved profiles only makes sense once more than one can drive the design agent. -const switchableProfiles = computed(designModelProfiles) -const canSwitchProfile = computed(() => switchableProfiles.value.length > 1) +// The composer switches configured Design-role profiles; raw provider model selection lives in Settings. const selectedProfileName = computed( () => designModelProfile.value?.name ?? selectedModelName.value ) @@ -284,26 +281,11 @@ function handleSubmit(e: Event) { {{ agentName }} - + -
- - {{ selectedModelName }} -
- - - diff --git a/src/components/chat/ChatProfileSelect.vue b/src/components/chat/ChatProfileSelect.vue index b33a2caed..eaac201dc 100644 --- a/src/components/chat/ChatProfileSelect.vue +++ b/src/components/chat/ChatProfileSelect.vue @@ -10,9 +10,15 @@ import { SelectViewport } from 'reka-ui' +import { AI_PROVIDERS } from '@open-pencil/core/constants' import { useI18n } from '@open-pencil/vue' -import { aiModelSettings, designModelProfiles, setModelRoleAssignment } from '@/app/ai/models' +import { + aiModelSettings, + designModelProfiles, + modelConnection, + setModelRoleAssignment +} from '@/app/ai/models' import type { AIModelProfileId } from '@/app/ai/models' import AppBadge from '@/components/ui/AppBadge.vue' import { useSelectUI } from '@/components/ui/select' @@ -27,6 +33,13 @@ const selectedProfileId = computed({ set: (profileId: AIModelProfileId) => setModelRoleAssignment('design', profileId) }) +function providerName(connectionId: string): string { + const providerID = modelConnection(connectionId)?.providerID + return providerID + ? (AI_PROVIDERS.find((provider) => provider.id === providerID)?.name ?? providerID) + : '' +} + const selectCls = useSelectUI({ trigger: 'min-w-0 max-w-full gap-1 rounded border-none bg-transparent px-1.5 py-0.5 text-[10px] text-muted', @@ -49,13 +62,22 @@ const selectCls = useSelectUI({ +
+ {{ ai.modelRoleDesign }} +
- {{ profile.name }} + + {{ profile.name }} + + {{ providerName(profile.connectionId) }} + + + {{ ai.modelCapabilityToolsShort }} {{ ai.modelCapabilityVisionShort }} diff --git a/src/components/ui/InputGroup.vue b/src/components/ui/InputGroup.vue index 7c5034eec..68fd0f617 100644 --- a/src/components/ui/InputGroup.vue +++ b/src/components/ui/InputGroup.vue @@ -30,7 +30,11 @@ const cls = computed(() => inputGroup({ size, disabled })) :data-disabled="disabled || undefined" :class="cls.root({ class: ui?.root })" > -
+
diff --git a/src/theme/input-group.ts b/src/theme/input-group.ts index 9539e5942..aceff2fc4 100644 --- a/src/theme/input-group.ts +++ b/src/theme/input-group.ts @@ -3,7 +3,7 @@ import { controlHeight } from '@/theme/control' export default { slots: { root: 'min-w-0 rounded-xl border border-border bg-input transition-colors hover:border-muted/60 focus-within:border-panel-focus focus-within:ring-1 focus-within:ring-accent/30', - attachment: 'px-2 pt-2', + attachment: 'm-2 mb-0', control: 'min-w-0', toolbar: 'flex min-w-0 items-center gap-1 px-1.5 pb-1.5', model: 'min-w-0 flex-1', diff --git a/tests/e2e/chat/panel.spec.ts b/tests/e2e/chat/panel.spec.ts index 5651497ba..d83f7c986 100644 --- a/tests/e2e/chat/panel.spec.ts +++ b/tests/e2e/chat/panel.spec.ts @@ -410,17 +410,18 @@ test('assistant Markdown restricts images and blocks unsafe link protocols', asy ) }) -test('model selector is visible and clickable', async () => { - const trigger = page.getByTestId('chat-model-selector') +test('design profile selector is visible and exposes role capabilities', async () => { + const trigger = page.getByTestId('chat-profile-selector') await expect(trigger).toBeVisible() await trigger.click() - await expect(page.getByRole('option', { name: /Claude Sonnet 4\.6/ })).toBeVisible() - await expect(page.getByText('Best for design')).toBeVisible() - await expect(page.getByText('Free').first()).toBeVisible() + await expect(page.getByText('Design agent', { exact: true })).toBeVisible() + await expect(page.getByRole('option', { name: /Claude Sonnet/ })).toContainText('OpenRouter') + await expect(page.getByRole('option', { name: /Claude Sonnet/ })).toContainText('Tools') + await expect(page.getByRole('option', { name: /Claude Sonnet/ })).toContainText('Vision') - await page.getByRole('option', { name: /Claude Sonnet 4\.6/ }).click() - await expect(page.getByRole('option', { name: /Claude Sonnet 4\.6/ })).toBeHidden() + await page.getByRole('option', { name: /Claude Sonnet/ }).click() + await expect(page.getByRole('option', { name: /Claude Sonnet/ })).toBeHidden() }) test('reasoning and response copy actions render in assistant messages', async () => { @@ -490,8 +491,7 @@ test('OpenRouter accepts a custom model ID from provider settings', async () => await page.getByRole('button', { name: 'Save model' }).click() await page.getByTestId('app-settings-done').click() - await expect(page.getByTestId('chat-custom-model-label')).toContainText(customModel) - await expect(page.getByTestId('chat-model-selector')).toBeHidden() + await expect(page.getByTestId('chat-profile-selector')).toContainText('Claude Sonnet') await page.getByTestId('provider-settings-trigger').click() await page.locator('[data-model-id]').first().click() @@ -502,7 +502,7 @@ test('OpenRouter accepts a custom model ID from provider settings', async () => await page.getByRole('button', { name: 'Save model' }).click() await page.getByTestId('app-settings-done').click() - await expect(page.getByTestId('chat-model-selector')).toBeVisible() + await expect(page.getByTestId('chat-profile-selector')).toBeVisible() }) test('transport errors show a safe localized toast', async () => {