fix(ui): restore searchable credential model picker

Use each credential's model IDs in the model name field and filter matches while typing.

Fixes #
This commit is contained in:
lzt404 committed 2026-10-06 15:36:29 +08:00
1 parent 9e5ed4d2d3
commit 36b98d2d0a
7 files changed
+25 -14

No files matched your search

+3 -2
View File
@@ -58,7 +58,8 @@
.credential-alias-list, .credential-header-list { display: grid; gap: 12px; }
.credential-alias-card { display: grid; gap: 8px; }
.credential-alias-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; gap: 8px; align-items: center; }
.credential-alias-row input { min-width: 0; }
.credential-alias-row input,
.credential-alias-row .agent-model-picker { min-width: 0; }
.credential-alias-row-actions { display: flex; align-items: center; gap: 4px; }
.credential-alias-row-actions .is-expanded { transform: rotate(180deg); }
.credential-alias-details { display: grid; gap: 10px; padding: 12px; border: 1px solid var(--border-color); border-radius: 10px; background: var(--bg-secondary); }
@@ -73,6 +74,6 @@
@media (max-width: 560px) {
.credential-alias-row { grid-template-columns: minmax(0, 1fr) auto; }
.credential-alias-row input:first-child { grid-column: 1 / -1; }
.credential-alias-row > :first-child { grid-column: 1 / -1; }
}
+4 -2
View File
@@ -7,7 +7,7 @@ import { loadAuthFileSettings, saveAuthFileSettings, type AuthFileSettingsDraft,
import { modelMatchesRule, normalizeOAuthExcludedRules, oauthModelCandidates, oauthModelsFromPayload, setOAuthModelsExcluded, type OAuthModelDefinition } from '../services/oauthModels';
import './AuthFileSettingsDialog.css';
import { CredentialAdvancedFields, CredentialHeadersEditor, credentialProviderKey } from './CredentialAdvancedFields';
import { modelSearchText } from '../services/modelService';
import { modelSearchText, type ModelOption } from '../services/modelService';
export function AuthFileSettingsDialog({ name, provider = '', onClose, onSaved }: {
name: string;
@@ -22,6 +22,7 @@ export function AuthFileSettingsDialog({ name, provider = '', onClose, onSaved }
const [original, setOriginal] = useState<AuthFileSettingsDraft | null>(null);
const [draft, setDraft] = useState<AuthFileSettingsDraft | null>(null);
const [models, setModels] = useState<OAuthModelDefinition[]>([]);
const [pickerModels, setPickerModels] = useState<ModelOption[]>([]);
const [catalogError, setCatalogError] = useState('');
const [catalogLoading, setCatalogLoading] = useState(true);
const [search, setSearch] = useState('');
@@ -59,6 +60,7 @@ export function AuthFileSettingsDialog({ name, provider = '', onClose, onSaved }
const catalog = oauthModelsFromPayload(payload);
if (!active) return;
setModels(catalog);
setPickerModels(catalog.map((model) => ({ name: model.id, alias: model.displayName })));
}).catch((reason: unknown) => {
if (active) setCatalogError(reason instanceof Error ? reason.message : String(reason));
}).finally(() => { if (active) setCatalogLoading(false); });
@@ -169,7 +171,7 @@ export function AuthFileSettingsDialog({ name, provider = '', onClose, onSaved }
</div>
</details>
</section>
<CredentialAdvancedFields name={name} provider={provider} advanced={draft.advanced} disabled={saving} onChange={(advanced) => update('advanced', advanced)} />
<CredentialAdvancedFields name={name} provider={provider} advanced={draft.advanced} models={pickerModels} modelsLoading={catalogLoading} modelsError={catalogError} disabled={saving} onChange={(advanced) => update('advanced', advanced)} />
<section className="credential-settings-section">
<h3>{t('authFiles.settings.additional')}</h3>
<CredentialHeadersEditor value={draft.headers} disabled={saving} onChange={(headers) => update('headers', headers)} />
+9 -3
View File
@@ -1,5 +1,7 @@
import { useState } from 'react';
import { ChevronDown, Plus, Undo2, X } from 'lucide-react';
import { AgentModelPicker } from './AgentModelPicker';
import type { ModelOption } from '../services/modelService';
import { useI18n } from '../i18n';
import type { MessageKey } from '../i18n/resources';
import { isRecord } from '../services/managementApi';
@@ -42,11 +44,14 @@ function aliasRows(value: unknown): AliasRow[] {
}
export function CredentialAdvancedFields({
name, provider = '', advanced, onChange, disabled = false,
name, provider = '', advanced, models = [], modelsLoading = false, modelsError = '', onChange, disabled = false,
}: {
name: string;
provider?: string;
advanced: Advanced;
models?: ModelOption[];
modelsLoading?: boolean;
modelsError?: string;
onChange: (advanced: Advanced) => void;
disabled?: boolean;
}) {
@@ -101,8 +106,9 @@ export function CredentialAdvancedFields({
const expanded = expandedAlias === index;
return <div className="credential-alias-card" key={index}>
<div className="credential-alias-row">
<input aria-label={t('authFiles.settings.aliasUpstream')} placeholder={t('authFiles.settings.aliasUpstream')} value={textValue(row.name)} disabled={disabled} autoComplete="off" spellCheck={false} maxLength={240}
onChange={(event) => updateAlias(index, { name: event.currentTarget.value })} />
<AgentModelPicker models={models} value={textValue(row.name)} loading={modelsLoading} error={modelsError} disabled={disabled}
editable={{ label: t('authFiles.settings.aliasUpstream'), placeholder: t('authFiles.settings.aliasModelPlaceholder'), maxLength: 240 }}
menuClassName="credential-model-menu" onChange={(value) => updateAlias(index, { name: value })} />
<input aria-label={t('authFiles.settings.aliasClient')} placeholder={t('authFiles.settings.aliasClient')} value={textValue(row.alias)} disabled={disabled} autoComplete="off" spellCheck={false} maxLength={240}
onChange={(event) => updateAlias(index, { alias: event.currentTarget.value })} />
<div className="credential-alias-row-actions">
+1
View File
@@ -88,6 +88,7 @@ export const jaOverrides = {
'authFiles.settings.aliasRemove': "別名 {index} を削除",
'authFiles.settings.aliasUpstream': "モデル名",
'authFiles.settings.aliasClient': "モデル別名",
'authFiles.settings.aliasModelPlaceholder': "モデルを検索または入力",
'authFiles.settings.aliasDisplay': "表示名",
'authFiles.settings.aliasFork': "モデル一覧に元のモデルを残す",
'authFiles.settings.aliasForce': "応答のモデル名を別名に書き換える",
+1
View File
@@ -88,6 +88,7 @@ export const en: Record<MessageKey, string> = {
'authFiles.settings.aliasRemove': "Remove alias {index}",
'authFiles.settings.aliasUpstream': "Model name",
'authFiles.settings.aliasClient': "Model alias",
'authFiles.settings.aliasModelPlaceholder': "Search or enter a model",
'authFiles.settings.aliasDisplay': "Display name",
'authFiles.settings.aliasFork': "Keep the original model in the model list",
'authFiles.settings.aliasForce': "Rewrite response model names to the alias",
+1
View File
@@ -87,6 +87,7 @@ export const zhCN = {
'authFiles.settings.aliasRemove': "删除别名 {index}",
'authFiles.settings.aliasUpstream': "模型名称",
'authFiles.settings.aliasClient': "模型别名",
'authFiles.settings.aliasModelPlaceholder': "搜索或输入模型",
'authFiles.settings.aliasDisplay': "显示名称",
'authFiles.settings.aliasFork': "在模型列表中保留原始模型",
'authFiles.settings.aliasForce': "将响应模型名称改写为别名",
+6 -7
View File
@@ -5,19 +5,18 @@ import { I18nProvider } from '../src/i18n';
const html = renderToStaticMarkup(
<I18nProvider>
<CredentialAdvancedFields name="codex-user.json" advanced={{ model_aliases: [{ name: 'gpt-5', alias: 'public-gpt' }] }} onChange={() => {}} />
<CredentialAdvancedFields name="codex-user.json" advanced={{ model_aliases: [{ name: 'gpt-5', alias: 'public-gpt' }] }}
models={[{ name: 'gpt-5', alias: 'GPT 5' }]} onChange={() => {}} />
</I18nProvider>,
);
describe('credential alias editor', () => {
it('uses plain inputs for the upstream model and client alias', () => {
expect(html).not.toContain('role="combobox"');
describe('credential alias model picker', () => {
it('uses the credential model list for the upstream model and a plain input for the alias', () => {
expect(html.match(/role="combobox"/g)).toHaveLength(1);
expect(html).toContain('aria-label="Model name"');
expect(html).toContain('aria-label="Model alias"');
expect(html).toContain('value="gpt-5"');
expect(html).toContain('value="public-gpt"');
expect(html).toContain('aria-label="Expand"');
expect(html).not.toContain('Display name');
expect(html).not.toContain('Search or enter a model');
expect(html).toContain('Search or enter a model');
});
});