feat(web): group default model dropdown by provider in settings (#861)

* feat(web): group default model dropdown by provider in settings

* fix(web): prevent login dialog closing on backdrop click
This commit is contained in:
qer 2026-06-17 23:48:23 +08:00 committed by GitHub
parent 0e2877bee3
commit bd0979578b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 74 additions and 14 deletions

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Prevent the web login dialog from closing when clicking the backdrop.

View file

@ -0,0 +1,5 @@
---
"@moonshot-ai/kimi-code": patch
---
Group the default model dropdown in web settings by provider.

View file

@ -197,7 +197,7 @@ function formatSeconds(s: number): string {
</script>
<template>
<div class="backdrop" @click.self="close">
<div class="backdrop">
<div ref="dialogRef" class="dialog" role="dialog" aria-modal="true" tabindex="-1" :aria-label="t('login.title')">
<!-- Header -->

View file

@ -76,19 +76,42 @@ function exportLog(): void {
downloadTraceLog();
}
type ModelOption = { id: string; label: string };
type ModelOption = { id: string; label: string; provider: string };
const modelOptions = computed<ModelOption[]>(() => {
const byId = new Map<string, string>();
const byId = new Map<string, ModelOption>();
for (const model of props.models ?? []) {
byId.set(model.id, model.displayName ?? model.model ?? model.id);
byId.set(model.id, {
id: model.id,
label: model.displayName ?? model.model ?? model.id,
provider: model.provider,
});
}
for (const [id, raw] of Object.entries(props.config?.models ?? {})) {
if (byId.has(id)) continue;
byId.set(id, formatConfigModelLabel(id, raw));
const provider = extractConfigModelProvider(raw);
byId.set(id, {
id,
label: formatConfigModelLabel(id, raw, provider),
provider: provider ?? id,
});
}
return Array.from(byId, ([id, label]) => ({ id, label }))
.sort((a, b) => a.label.localeCompare(b.label));
return Array.from(byId.values());
});
const modelGroups = computed<Array<{ provider: string; options: ModelOption[] }>>(() => {
const map = new Map<string, ModelOption[]>();
for (const option of modelOptions.value) {
const list = map.get(option.provider) ?? [];
list.push(option);
map.set(option.provider, list);
}
for (const list of map.values()) {
list.sort((a, b) => a.label.localeCompare(b.label));
}
return Array.from(map.entries())
.toSorted(([a], [b]) => a.localeCompare(b))
.map(([provider, options]) => ({ provider, options }));
});
const providerEntries = computed<Array<{ id: string; provider: AppConfigProvider }>>(() =>
@ -102,12 +125,19 @@ const defaultPermissionMode = computed(() => {
return mode === 'auto' || mode === 'yolo' || mode === 'manual' ? mode : 'manual';
});
function formatConfigModelLabel(id: string, raw: unknown): string {
function extractConfigModelProvider(raw: unknown): string | undefined {
if (!raw || typeof raw !== 'object') return undefined;
const source = raw as Record<string, unknown>;
const provider = typeof source['provider'] === 'string' ? source['provider'] : undefined;
return provider;
}
function formatConfigModelLabel(id: string, raw: unknown, provider?: string): string {
if (!raw || typeof raw !== 'object') return id;
const source = raw as Record<string, unknown>;
const model = typeof source['model'] === 'string' ? source['model'] : undefined;
const provider = typeof source['provider'] === 'string' ? source['provider'] : undefined;
if (model && provider) return `${id} (${provider}/${model})`;
const resolvedProvider = provider ?? extractConfigModelProvider(raw);
if (model && resolvedProvider) return `${id} (${resolvedProvider}/${model})`;
if (model) return `${id} (${model})`;
return id;
}
@ -271,7 +301,7 @@ function setTab(tab: SettingsTab): void {
<span class="hint">{{ t('settings.defaultModelHint') }}</span>
</span>
<select
v-if="modelOptions.length > 0"
v-if="modelGroups.length > 0"
class="select-field"
:value="config.defaultModel ?? ''"
:disabled="configSaving"
@ -279,9 +309,11 @@ function setTab(tab: SettingsTab): void {
@change="setDefaultModel"
>
<option v-if="!config.defaultModel" value="" disabled>{{ t('settings.noDefaultModel') }}</option>
<option v-for="model in modelOptions" :key="model.id" :value="model.id">
{{ model.label }}
</option>
<optgroup v-for="group in modelGroups" :key="group.provider" :label="group.provider">
<option v-for="model in group.options" :key="model.id" :value="model.id">
{{ model.label }}
</option>
</optgroup>
</select>
<span v-else class="rvalue mono">{{ config.defaultModel ?? t('settings.noDefaultModel') }}</span>
</div>

View file

@ -162,6 +162,24 @@ describe('SettingsDialog config controls', () => {
await planRow!.find('button.switch').trigger('click');
expect(wrapper.emitted('updateConfig')?.[2]?.[0]).toEqual({ defaultPlanMode: true });
});
it('groups default model options by provider', async () => {
const wrapper = mountDialog();
const agentTab = wrapper.findAll('.tab').find((button) => button.text() === 'Agent');
await agentTab!.trigger('click');
const groups = wrapper.findAll('optgroup');
expect(groups.length).toBe(2);
expect(groups[0]!.attributes('label')).toBe('kimi');
expect(groups[1]!.attributes('label')).toBe('openai');
const kimiOptions = groups[0]!.findAll('option');
expect(kimiOptions.some((o) => o.attributes('value') === 'kimi/k2')).toBe(true);
const openaiOptions = groups[1]!.findAll('option');
expect(openaiOptions.some((o) => o.attributes('value') === 'openai/gpt-5')).toBe(true);
});
});
describe('SettingsDialog dialog focus', () => {