mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-07-09 17:29:12 +00:00
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:
parent
0e2877bee3
commit
bd0979578b
5 changed files with 74 additions and 14 deletions
5
.changeset/disable-login-backdrop-close.md
Normal file
5
.changeset/disable-login-backdrop-close.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Prevent the web login dialog from closing when clicking the backdrop.
|
||||
5
.changeset/group-settings-model-by-provider.md
Normal file
5
.changeset/group-settings-model-by-provider.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@moonshot-ai/kimi-code": patch
|
||||
---
|
||||
|
||||
Group the default model dropdown in web settings by provider.
|
||||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue