diff --git a/apps/desktop/src/views/SettingsView/components/ModelPreferences/index.vue b/apps/desktop/src/views/SettingsView/components/ModelPreferences/index.vue
new file mode 100644
index 00000000..c4a2bc36
--- /dev/null
+++ b/apps/desktop/src/views/SettingsView/components/ModelPreferences/index.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
diff --git a/apps/desktop/src/views/SettingsView/index.vue b/apps/desktop/src/views/SettingsView/index.vue
index 39a329fd..ce46215f 100644
--- a/apps/desktop/src/views/SettingsView/index.vue
+++ b/apps/desktop/src/views/SettingsView/index.vue
@@ -26,6 +26,9 @@
});
const GeneralView = defineAsyncComponent(() => import('./components/General/index.vue'));
+ const ModelPreferencesView = defineAsyncComponent(
+ () => import('./components/ModelPreferences/index.vue')
+ );
const AiServicesView = defineAsyncComponent(() => import('./components/AiServices/index.vue'));
const BuiltInToolsView = defineAsyncComponent(
() => import('./components/BuiltInTools/index.vue')
@@ -58,6 +61,11 @@
component: AiServicesView,
loadingKey: 'settings.loading.aiServices',
},
+ 'model-preferences': {
+ component: ModelPreferencesView,
+ loadingKey: 'settings.loading.modelPreferences',
+ scrollable: true,
+ },
'built-in-tools': {
component: BuiltInToolsView,
loadingKey: 'settings.loading.builtInTools',
diff --git a/apps/desktop/src/views/SettingsView/settingsNavigation.ts b/apps/desktop/src/views/SettingsView/settingsNavigation.ts
index 013bb9bc..f8b4f8eb 100644
--- a/apps/desktop/src/views/SettingsView/settingsNavigation.ts
+++ b/apps/desktop/src/views/SettingsView/settingsNavigation.ts
@@ -5,6 +5,7 @@ import { JSON_SETTINGS_SECTIONS } from '@/stores/setting/sections/registry';
export type NavigationSection =
| 'general'
+ | 'model-preferences'
| 'ai-services'
| 'built-in-tools'
| 'search'
@@ -64,10 +65,16 @@ const settingsNavigationDefinitions: SettingsNavigationGroupDefinition[] = [
items: [
{
id: 'ai-services',
- icon: 'llm',
+ icon: 'cloud',
labelKey: 'settings.nav.aiServices.label',
descriptionKey: 'settings.nav.aiServices.description',
},
+ {
+ id: 'model-preferences',
+ icon: 'route',
+ labelKey: 'settings.nav.modelPreferences.label',
+ descriptionKey: 'settings.nav.modelPreferences.description',
+ },
{
id: 'built-in-tools',
icon: 'wrench',
diff --git a/apps/desktop/tests/SettingsView/ai-services-i18n.test.ts b/apps/desktop/tests/SettingsView/ai-services-i18n.test.ts
index 18fcdae7..a279d3df 100644
--- a/apps/desktop/tests/SettingsView/ai-services-i18n.test.ts
+++ b/apps/desktop/tests/SettingsView/ai-services-i18n.test.ts
@@ -188,19 +188,19 @@ describe('AiServices i18n and layout', () => {
setLocale('zh-CN');
});
- it('renders provider list chrome and default badge in English with wrapping-safe classes', () => {
+ it('renders provider list chrome in English with wrapping-safe classes', () => {
setLocale('en-US');
const wrapper = mount(ProviderList, {
props: {
providers: [createProvider({ is_builtin: 1 })],
selectedProviderId: 1,
- defaultModelProviderIds: new Set([1]),
+ defaultModelProviderIds: new Set
(),
},
});
expect(wrapper.text()).toContain('Add custom provider');
- expect(wrapper.text()).toContain('Default');
+ expect(wrapper.text()).toContain('Built-in');
expect(wrapper.text()).not.toContain('大模型服务');
const addButton = wrapper.get('button');
@@ -314,7 +314,7 @@ describe('AiServices i18n and layout', () => {
props: {
providers: [createProvider({ name: '服务商', is_builtin: 1 })],
selectedProviderId: 1,
- defaultModelProviderIds: new Set([1]),
+ defaultModelProviderIds: new Set(),
},
attachTo: document.body,
});
@@ -322,7 +322,7 @@ describe('AiServices i18n and layout', () => {
const localizer = createDomLocalizer(document.body);
localizer.translateNow();
- expect(wrapper.text()).toContain('Default');
+ expect(wrapper.text()).toContain('Built-in');
expect(wrapper.get('.provider-card h3').text()).toBe('服务商');
expect(wrapper.get('.provider-card h3').attributes('data-no-i18n')).toBe('true');
diff --git a/apps/desktop/tests/SettingsView/ai-services-section-i18n.test.ts b/apps/desktop/tests/SettingsView/ai-services-section-i18n.test.ts
index 44abbb77..b8069207 100644
--- a/apps/desktop/tests/SettingsView/ai-services-section-i18n.test.ts
+++ b/apps/desktop/tests/SettingsView/ai-services-section-i18n.test.ts
@@ -52,6 +52,9 @@ vi.mock('@database/queries', () => ({
findAllProvidersSorted: vi.fn(async () => []),
findDefaultModel: vi.fn(async () => null),
findModelsWithProvider: vi.fn(async () => []),
+ findProviderById: vi.fn(async () => null),
+ getSettingValue: vi.fn(async () => null),
+ listModelPreferences: vi.fn(async () => []),
setDefaultModel: vi.fn(),
syncAllModelsMetadata: vi.fn(),
updateModel: vi.fn(),
diff --git a/apps/desktop/tests/SettingsView/model-card-i18n.test.ts b/apps/desktop/tests/SettingsView/model-card-i18n.test.ts
index 306f3173..5909a63f 100644
--- a/apps/desktop/tests/SettingsView/model-card-i18n.test.ts
+++ b/apps/desktop/tests/SettingsView/model-card-i18n.test.ts
@@ -103,10 +103,8 @@ describe('ModelCard i18n', () => {
},
});
- const defaultRadio = wrapper.get('input[type="radio"]');
const buttons = wrapper.findAll('button');
- expect(defaultRadio.attributes('title')).toBe('Enable this provider first');
expect(buttons[0]?.attributes('title')).toBe('Edit');
expect(buttons[1]?.attributes('title')).toBe('Delete');
diff --git a/apps/desktop/tests/SettingsView/navigation-sidebar-i18n.test.ts b/apps/desktop/tests/SettingsView/navigation-sidebar-i18n.test.ts
index b432e49c..656d2647 100644
--- a/apps/desktop/tests/SettingsView/navigation-sidebar-i18n.test.ts
+++ b/apps/desktop/tests/SettingsView/navigation-sidebar-i18n.test.ts
@@ -62,6 +62,7 @@ describe('Settings navigation sidebar i18n', () => {
expect(flattenSettingsNavigation().map((item) => item.label)).toEqual([
'General',
'Providers and models',
+ 'Model routing',
'Built-in tools',
'Search',
'Browser Control',
diff --git a/apps/desktop/tests/services/AgentService/prompt/model-preferences.test.ts b/apps/desktop/tests/services/AgentService/prompt/model-preferences.test.ts
new file mode 100644
index 00000000..bbe0595b
--- /dev/null
+++ b/apps/desktop/tests/services/AgentService/prompt/model-preferences.test.ts
@@ -0,0 +1,130 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { buildModelPreferencesPrompt } from '@/services/AgentService/prompt/modelPreferences';
+
+const queries = vi.hoisted(() => ({
+ findModelRoleWithProvider: vi.fn(),
+ getSettingValue: vi.fn(),
+ listModelPreferences: vi.fn(),
+}));
+
+vi.mock('@database/queries', () => queries);
+
+function createModel(overrides = {}) {
+ return {
+ id: 1,
+ provider_id: 1,
+ model_id: 'model-a',
+ name: 'Model A',
+ provider_name: 'Provider A',
+ provider_enabled: 1,
+ ...overrides,
+ };
+}
+
+function createPreference(overrides = {}) {
+ return {
+ id: 1,
+ name: '前端开发',
+ description: 'React、Vue、CSS、Tailwind',
+ provider_id: 1,
+ model_id: 10,
+ priority: 0,
+ created_at: '',
+ updated_at: '',
+ model_name: 'Claude Sonnet',
+ model_api_id: 'claude-sonnet',
+ model_provider_id: 1,
+ provider_name: 'Anthropic',
+ provider_enabled: 1,
+ ...overrides,
+ };
+}
+
+describe('model preferences prompt', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ queries.getSettingValue.mockResolvedValue('true');
+ queries.findModelRoleWithProvider.mockImplementation((role: string) => {
+ if (role === 'entry') {
+ return Promise.resolve(createModel({ name: 'Default Model' }));
+ }
+ if (role === 'fast') {
+ return Promise.resolve(createModel({ name: 'Fast Model' }));
+ }
+ if (role === 'general') {
+ return Promise.resolve(createModel({ name: 'General Model' }));
+ }
+ return Promise.resolve(null);
+ });
+ queries.listModelPreferences.mockResolvedValue([createPreference()]);
+ });
+
+ it('injects model roles and usable scenario preferences', async () => {
+ const [prompt] = await buildModelPreferencesPrompt();
+
+ expect(prompt).toContain('## Model routing preferences');
+ expect(prompt).toContain('Default model');
+ expect(prompt).toContain('Fast model');
+ expect(prompt).toContain('General model');
+ expect(prompt).toContain('Provider A / Default Model');
+ expect(prompt).toContain('Provider A / Fast Model');
+ expect(prompt).toContain('Provider A / General Model');
+ expect(prompt).toContain(
+ '| 前端开发 | React、Vue、CSS、Tailwind | Anthropic / Claude Sonnet |'
+ );
+ expect(prompt).toContain('{ "scenario": "" }');
+ expect(prompt).not.toContain('{ "scenario": null }');
+ });
+
+ it('omits routing preferences when model routing is disabled', async () => {
+ queries.getSettingValue.mockResolvedValue('false');
+
+ const prompt = await buildModelPreferencesPrompt();
+
+ expect(prompt).toEqual([]);
+ expect(queries.listModelPreferences).not.toHaveBeenCalled();
+ });
+
+ it('omits unusable scenario preferences without failing prompt construction', async () => {
+ queries.listModelPreferences.mockResolvedValue([
+ createPreference({ model_id: null }),
+ createPreference({ name: '翻译', provider_enabled: 0 }),
+ ]);
+
+ const [prompt] = await buildModelPreferencesPrompt();
+
+ expect(prompt).toContain(
+ '| None configured | No custom scenario preferences are available. | - |'
+ );
+ expect(prompt).not.toContain('| 翻译 |');
+ });
+
+ it('escapes markdown table control characters in user-configured text', async () => {
+ queries.listModelPreferences.mockResolvedValue([
+ createPreference({
+ name: 'Frontend | UI',
+ description: 'React\\Vue\nCSS | Tailwind',
+ provider_name: 'Provider\\A',
+ model_name: 'Model | A',
+ }),
+ ]);
+
+ const [prompt] = await buildModelPreferencesPrompt();
+
+ expect(prompt).toContain(
+ '| Frontend \\| UI | React\\\\Vue CSS \\| Tailwind | Provider\\\\A / Model \\| A |'
+ );
+ });
+
+ it('falls back to an empty preference prompt when preference queries fail', async () => {
+ queries.listModelPreferences.mockRejectedValue(new Error('database unavailable'));
+
+ const [prompt] = await buildModelPreferencesPrompt();
+
+ expect(prompt).toContain('## Model routing preferences');
+ expect(prompt).toContain(
+ '| None configured | No custom scenario preferences are available. | - |'
+ );
+ });
+});
diff --git a/apps/desktop/tests/services/BuiltInToolService/tools/upgradeModel/i18n.test.ts b/apps/desktop/tests/services/BuiltInToolService/tools/upgradeModel/i18n.test.ts
index 7c675875..a7ab8634 100644
--- a/apps/desktop/tests/services/BuiltInToolService/tools/upgradeModel/i18n.test.ts
+++ b/apps/desktop/tests/services/BuiltInToolService/tools/upgradeModel/i18n.test.ts
@@ -12,12 +12,29 @@ import {
} from '@/services/BuiltInToolService/tools/upgradeModel/chain';
import type { BaseBuiltInToolExecutionContext } from '@/services/BuiltInToolService/types';
-const { findModelByProviderAndModelIdMock } = vi.hoisted(() => ({
+const {
+ findDefaultModelWithProviderMock,
+ findEffectiveModelRoleWithProviderMock,
+ findModelByIdWithProviderMock,
+ findModelByProviderAndModelIdMock,
+ findModelPreferenceByNameMock,
+ getSettingValueMock,
+} = vi.hoisted(() => ({
+ findDefaultModelWithProviderMock: vi.fn(),
+ findEffectiveModelRoleWithProviderMock: vi.fn(),
+ findModelByIdWithProviderMock: vi.fn(),
findModelByProviderAndModelIdMock: vi.fn(),
+ findModelPreferenceByNameMock: vi.fn(),
+ getSettingValueMock: vi.fn(),
}));
vi.mock('@database/queries', () => ({
+ findDefaultModelWithProvider: findDefaultModelWithProviderMock,
+ findEffectiveModelRoleWithProvider: findEffectiveModelRoleWithProviderMock,
+ findModelByIdWithProvider: findModelByIdWithProviderMock,
findModelByProviderAndModelId: findModelByProviderAndModelIdMock,
+ findModelPreferenceByName: findModelPreferenceByNameMock,
+ getSettingValue: getSettingValueMock,
}));
function createModel(overrides: Partial): ModelWithProvider {
@@ -65,9 +82,10 @@ describe('UpgradeModel i18n', () => {
beforeEach(() => {
setLocale('zh-CN');
vi.clearAllMocks();
+ getSettingValueMock.mockResolvedValue('true');
});
- it('creates an English approval request for the resolved target model', async () => {
+ it('does not create an approval request for model routing switches', async () => {
setLocale('en-US');
const currentModel = createModel({
id: 10,
@@ -76,17 +94,6 @@ describe('UpgradeModel i18n', () => {
name: 'Model A',
provider_name: 'Provider A',
});
- const targetModel = createModel({
- id: 20,
- provider_id: 2,
- model_id: 'model-b',
- name: 'Model B',
- provider_name: 'Provider B',
- });
- findModelByProviderAndModelIdMock
- .mockResolvedValueOnce(currentModel)
- .mockResolvedValueOnce(targetModel);
-
const approval = await buildUpgradeModelApprovalRequest(
{},
{
@@ -99,14 +106,21 @@ describe('UpgradeModel i18n', () => {
createContext(currentModel)
);
- expect(approval).toMatchObject({
- title: 'Confirm model switch',
- description: 'Allow switching from Provider A / Model A to Provider B / Model B',
- command: 'Provider A / Model A -> Provider B / Model B',
- reason: 'This changes the model used by the current conversation and also affects the subsequent default model.',
- approveLabel: 'Approve',
- rejectLabel: 'Reject',
+ expect(approval).toBeNull();
+ });
+
+ it('does not emit a switch signal when model routing is disabled', async () => {
+ setLocale('en-US');
+ getSettingValueMock.mockResolvedValue('false');
+
+ const result = await executeUpgradeModelTool({}, { chain: [] }, createContext());
+
+ expect(result).toMatchObject({
+ isError: true,
+ status: 'error',
+ errorMessage: 'Model routing is disabled. Continuing with the default model.',
});
+ expect(result.controlSignal).toBeUndefined();
});
it('formats reachable execute result strings in English while returning the model switch signal', async () => {
@@ -184,4 +198,133 @@ describe('UpgradeModel i18n', () => {
'Each model upgrade chain item must include providerId and a non-empty modelId.'
);
});
+
+ it('switches to a configured scenario model without exposing provider details to the model', async () => {
+ setLocale('en-US');
+ const targetModel = createModel({
+ id: 20,
+ provider_id: 2,
+ model_id: 'claude-sonnet',
+ name: 'Claude Sonnet',
+ provider_name: 'Anthropic',
+ });
+ findModelPreferenceByNameMock.mockResolvedValue({
+ id: 1,
+ name: 'Frontend',
+ description: 'React and CSS work',
+ model_id: 20,
+ });
+ findModelByIdWithProviderMock.mockResolvedValue(targetModel);
+
+ const result = await executeUpgradeModelTool(
+ { scenario: 'Frontend' },
+ { chain: [] },
+ createContext(createModel({ id: 10 }))
+ );
+
+ expect(findModelPreferenceByNameMock).toHaveBeenCalledWith('Frontend');
+ expect(result).toMatchObject({
+ isError: false,
+ status: 'success',
+ controlSignal: {
+ type: 'upgrade_model',
+ targetModel,
+ restartCurrentRequest: false,
+ },
+ });
+ expect(result.result).toContain('Switched model for scenario: Frontend');
+ expect(result.result).toContain('Target model: Anthropic / Claude Sonnet');
+ });
+
+ it('does not request approval or emit a switch signal when the target is the current model', async () => {
+ setLocale('en-US');
+ const currentModel = createModel({
+ id: 20,
+ provider_id: 2,
+ model_id: 'claude-sonnet',
+ name: 'Claude Sonnet',
+ provider_name: 'Anthropic',
+ });
+ findModelPreferenceByNameMock.mockResolvedValue({
+ id: 1,
+ name: 'Frontend',
+ description: 'React and CSS work',
+ model_id: 20,
+ });
+ findModelByIdWithProviderMock.mockResolvedValue(currentModel);
+
+ const approval = await buildUpgradeModelApprovalRequest(
+ { scenario: 'Frontend' },
+ { chain: [] },
+ 'builtin:upgrade_model',
+ createContext(currentModel)
+ );
+ const result = await executeUpgradeModelTool(
+ { scenario: 'Frontend' },
+ { chain: [] },
+ createContext(currentModel)
+ );
+
+ expect(approval).toBeNull();
+ expect(result).toMatchObject({
+ isError: false,
+ status: 'success',
+ });
+ expect(result.controlSignal).toBeUndefined();
+ expect(result.result).toContain('Model is already the target model');
+ });
+
+ it('skips approval when model routing is enabled', async () => {
+ setLocale('en-US');
+ getSettingValueMock.mockResolvedValueOnce('true');
+ const currentModel = createModel({
+ id: 10,
+ provider_id: 1,
+ model_id: 'model-a',
+ name: 'Model A',
+ provider_name: 'Provider A',
+ });
+ const targetModel = createModel({
+ id: 20,
+ provider_id: 2,
+ model_id: 'model-b',
+ name: 'Model B',
+ provider_name: 'Provider B',
+ });
+ findModelByProviderAndModelIdMock
+ .mockResolvedValueOnce(currentModel)
+ .mockResolvedValueOnce(targetModel);
+
+ const approval = await buildUpgradeModelApprovalRequest(
+ {},
+ {
+ chain: [
+ { providerId: 1, modelId: 'model-a' },
+ { providerId: 2, modelId: 'model-b' },
+ ],
+ },
+ 'builtin:upgrade_model',
+ createContext(currentModel)
+ );
+
+ expect(approval).toBeNull();
+ });
+
+ it('rejects conflicting model switch target selectors', async () => {
+ setLocale('en-US');
+
+ const result = await executeUpgradeModelTool(
+ { role: 'fast', scenario: 'Frontend' },
+ { chain: [] },
+ createContext()
+ );
+
+ expect(result).toMatchObject({
+ isError: true,
+ status: 'error',
+ errorMessage: 'Specify only one model switch target at a time',
+ });
+ expect(findEffectiveModelRoleWithProviderMock).not.toHaveBeenCalled();
+ expect(findModelPreferenceByNameMock).not.toHaveBeenCalled();
+ });
});
diff --git a/apps/desktop/tests/stores/setting.test.ts b/apps/desktop/tests/stores/setting.test.ts
index dfc96da4..c8b57b45 100644
--- a/apps/desktop/tests/stores/setting.test.ts
+++ b/apps/desktop/tests/stores/setting.test.ts
@@ -26,6 +26,7 @@ const EXPECTED_GENERAL_SETTING_KEYS: GeneralSettingKey[] = [
'global_shortcut',
'start_on_boot',
'start_minimized',
+ 'allow_model_auto_switch',
'output_scroll_behavior',
'search_window_size_preset',
'language',
@@ -130,6 +131,7 @@ describe('setting registry', () => {
'updateGlobalShortcut',
'updateStartOnBoot',
'updateStartMinimized',
+ 'updateAllowModelAutoSwitch',
'updateOutputScrollBehavior',
'updateSearchWindowSizePreset',
'updateLanguage',
diff --git a/apps/desktop/tests/views/SettingsView/settingsAiServicesLayout.test.ts b/apps/desktop/tests/views/SettingsView/settingsAiServicesLayout.test.ts
index 266bb26f..cebfa8ab 100644
--- a/apps/desktop/tests/views/SettingsView/settingsAiServicesLayout.test.ts
+++ b/apps/desktop/tests/views/SettingsView/settingsAiServicesLayout.test.ts
@@ -13,6 +13,8 @@ const queries = vi.hoisted(() => ({
findDefaultModel: vi.fn(),
findModelsWithProvider: vi.fn(),
findProviderById: vi.fn(),
+ getSettingValue: vi.fn(),
+ listModelPreferences: vi.fn(),
setDefaultModel: vi.fn(),
syncAllModelsMetadata: vi.fn(),
updateModel: vi.fn(),
@@ -106,6 +108,8 @@ describe('SettingsAiServicesSection', () => {
});
queries.findDefaultModel.mockResolvedValue(null);
queries.findModelsWithProvider.mockResolvedValue([]);
+ queries.getSettingValue.mockResolvedValue(null);
+ queries.listModelPreferences.mockResolvedValue([]);
modelMetadataMock.updateModelMetadata.mockResolvedValue(undefined);
});
@@ -350,7 +354,7 @@ describe('SettingsAiServicesSection', () => {
expect(alertMock.success).not.toHaveBeenCalled();
});
- it('patches the default model locally without provider list reload', async () => {
+ it('patches model edits locally without provider list reload', async () => {
const provider = {
id: 2,
name: 'Custom Gateway',
@@ -403,11 +407,11 @@ describe('SettingsAiServicesSection', () => {
ProviderConfig: true,
ModelList: {
props: ['defaultModelId'],
- emits: ['set-default'],
+ emits: ['update'],
template: `
@@ -425,14 +429,95 @@ describe('SettingsAiServicesSection', () => {
await flushPromises();
- expect(wrapper.get('[data-testid="set-default-model"]').text()).toContain('default: 10');
+ expect(wrapper.get('[data-testid="update-model"]').text()).toContain('default: 10');
- await wrapper.get('[data-testid="set-default-model"]').trigger('click');
+ await wrapper.get('[data-testid="update-model"]').trigger('click');
await flushPromises();
- expect(queries.setDefaultModel).toHaveBeenCalledWith({ modelId: 20 });
+ expect(queries.updateModel).toHaveBeenCalledWith({
+ id: 20,
+ modelPatch: { name: 'Renamed Model' },
+ });
expect(queries.findAllProvidersSorted).toHaveBeenCalledTimes(1);
- expect(alertMock.success).toHaveBeenCalledWith('设置成功');
- expect(wrapper.get('[data-testid="set-default-model"]').text()).toContain('default: 20');
+ expect(alertMock.success).toHaveBeenCalledWith('保存成功');
+ });
+ it('blocks deleting models referenced by model routing', async () => {
+ const provider = {
+ id: 2,
+ name: 'Custom Gateway',
+ driver: 'openai',
+ api_endpoint: 'https://custom.example.com',
+ api_key: 'sk-demo',
+ config_json: null,
+ logo: 'openai.png',
+ enabled: 1,
+ is_builtin: 0,
+ created_at: '',
+ updated_at: '',
+ };
+ const defaultModel = {
+ id: 10,
+ provider_id: 2,
+ name: 'GPT 5 Mini',
+ model_id: 'gpt-5-mini',
+ is_default: 1,
+ last_used_at: null,
+ attachment: 0,
+ modalities: null,
+ open_weights: 0,
+ reasoning: 0,
+ release_date: null,
+ temperature: 1,
+ tool_call: 1,
+ knowledge: null,
+ context_limit: null,
+ output_limit: null,
+ is_custom_metadata: 0,
+ created_at: '',
+ updated_at: '',
+ };
+ const routedModel = {
+ ...defaultModel,
+ id: 20,
+ name: 'GPT 5',
+ model_id: 'gpt-5',
+ is_default: 0,
+ };
+ queries.findAllProvidersSorted.mockResolvedValue([provider]);
+ queries.findDefaultModel.mockResolvedValue(defaultModel);
+ queries.findModelsWithProvider.mockResolvedValue([defaultModel, routedModel]);
+ queries.getSettingValue.mockImplementation(({ key }: { key: string }) =>
+ Promise.resolve(key === 'model_role_fast_model_id' ? '20' : null)
+ );
+
+ const wrapper = mount(AiServicesSection, {
+ global: {
+ stubs: {
+ ProviderList: true,
+ ProviderConfig: true,
+ ModelList: {
+ emits: ['delete'],
+ template: `
+
+ `,
+ },
+ AddProviderDialog: true,
+ EditProviderDialog: true,
+ BadgedLogo: {
+ props: ['logo', 'name'],
+ template: '',
+ },
+ },
+ },
+ });
+
+ await flushPromises();
+ await wrapper.get('[data-testid="delete-routed-model"]').trigger('click');
+ await flushPromises();
+
+ expect(queries.deleteModel).not.toHaveBeenCalled();
+ expect(alertMock.error).toHaveBeenCalled();
});
});
diff --git a/apps/desktop/tests/views/SettingsView/settingsGeneralComponent.test.ts b/apps/desktop/tests/views/SettingsView/settingsGeneralComponent.test.ts
index 05b0bd40..1af1e576 100644
--- a/apps/desktop/tests/views/SettingsView/settingsGeneralComponent.test.ts
+++ b/apps/desktop/tests/views/SettingsView/settingsGeneralComponent.test.ts
@@ -14,6 +14,7 @@ const settingsStoreMock = vi.hoisted(() => ({
outputScrollBehavior: 'follow_output',
searchWindowSizePreset: 'normal',
searchWindowDefaultSize: { width: 720, height: 520 },
+ allowModelAutoSwitch: false,
appUpdateChannel: 'stable',
appUpdateAutoCheck: true,
appUpdateLastCheckedAt: null,
@@ -25,6 +26,7 @@ const settingsStoreMock = vi.hoisted(() => ({
updateStartMinimized: vi.fn().mockResolvedValue(undefined),
updateOutputScrollBehavior: vi.fn().mockResolvedValue(undefined),
updateSearchWindowSizePreset: vi.fn().mockResolvedValue(undefined),
+ updateAllowModelAutoSwitch: vi.fn().mockResolvedValue(undefined),
updateLanguage: vi.fn().mockResolvedValue(undefined),
updateAppUpdateChannel: vi.fn().mockResolvedValue(undefined),
updateAppUpdateAutoCheck: vi.fn().mockResolvedValue(undefined),
@@ -152,6 +154,8 @@ describe('SettingsGeneralSection', () => {
appUpdateServiceMock.state = appUpdateServiceMock.createState();
nativeMock.shortcut.getShortcutStatus.mockResolvedValue([false, null]);
nativeMock.autostart.isAutostartEnabled.mockResolvedValue(false);
+ settingsStoreMock.settings.value.allowModelAutoSwitch = false;
+ settingsStoreMock.updateAllowModelAutoSwitch.mockResolvedValue(undefined);
});
it('renders the general settings groups and row controls', () => {
diff --git a/eslint.config.js b/eslint.config.js
index 4c733c6c..26dd29c0 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -66,6 +66,10 @@ export default defineConfig([
'.coverage/**',
'**/.coverage',
'**/.coverage/**',
+ '.vite-cache',
+ '.vite-cache/**',
+ '**/.vite-cache',
+ '**/.vite-cache/**',
'src-tauri',
'src-tauri/**',
'**/src-tauri',