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:
1 parent
9e5ed4d2d3
commit
36b98d2d0a
7 files changed
+25
-14
No files matched your search
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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)} />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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': "応答のモデル名を別名に書き換える",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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': "将响应模型名称改写为别名",
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user