fix(ui): prevent text overlap in Claude Code model selectors
- Let selector height adapt to model names and aliases - Add layout regression tests across languages, themes, and viewport sizes - Verify model selection, keyboard search, 1M toggles, and config updates
This commit is contained in:
1 parent
97ba1051a2
commit
f7e9bd9884
13 files changed
+208
-65
No files matched your search
@@ -52,7 +52,7 @@
|
||||
.claude-code-model-inputs .agent-model-trigger,
|
||||
.claude-role-table .agent-model-trigger {
|
||||
min-height: 40px;
|
||||
height: 40px;
|
||||
height: auto;
|
||||
padding: 7px 11px;
|
||||
border: 1px solid var(--theme-d5d2cb);
|
||||
border-radius: 6px;
|
||||
|
||||
@@ -50,7 +50,6 @@ export function ClaudeCodeModelInputs({ startupModel, subagentModel, models, rou
|
||||
<span className="switch-track" aria-hidden="true" /></span><span>1M</span>
|
||||
</label>
|
||||
</div>
|
||||
{preview.role && <small className="claude-code-model-summary" aria-live="polite">{preview.role} → {preview.model}</small>}
|
||||
{!loading && !isKnownClaudeCodeModel(value, models) ? (
|
||||
<small role="status">{t('agents.claudeCodeRuntime.unverifiedModel')}</small>
|
||||
) : null}
|
||||
|
||||
@@ -24,6 +24,10 @@ export const authFileListZhCN = {
|
||||
'authFiles.usage.title': '累计用量',
|
||||
'authFiles.toolbar.refreshList': '刷新列表',
|
||||
'authFiles.toolbar.refreshQuota': '刷新额度',
|
||||
'authFiles.action.refreshQuota': '刷新',
|
||||
'authFiles.action.settings': '设置',
|
||||
'authFiles.action.models': '模型',
|
||||
'authFiles.action.copy': '复制',
|
||||
} as const;
|
||||
|
||||
export const authFileListEn: Record<keyof typeof authFileListZhCN, string> = {
|
||||
@@ -52,6 +56,10 @@ export const authFileListEn: Record<keyof typeof authFileListZhCN, string> = {
|
||||
'authFiles.usage.title': 'Cumulative usage',
|
||||
'authFiles.toolbar.refreshList': 'Refresh list',
|
||||
'authFiles.toolbar.refreshQuota': 'Refresh quota',
|
||||
'authFiles.action.refreshQuota': 'Refresh',
|
||||
'authFiles.action.settings': 'Settings',
|
||||
'authFiles.action.models': 'Models',
|
||||
'authFiles.action.copy': 'Copy',
|
||||
};
|
||||
|
||||
export const authFileListJa: Record<keyof typeof authFileListZhCN, string> = {
|
||||
@@ -80,4 +88,8 @@ export const authFileListJa: Record<keyof typeof authFileListZhCN, string> = {
|
||||
'authFiles.usage.title': '累計使用量',
|
||||
'authFiles.toolbar.refreshList': '一覧を更新',
|
||||
'authFiles.toolbar.refreshQuota': 'クォータを更新',
|
||||
'authFiles.action.refreshQuota': '更新',
|
||||
'authFiles.action.settings': '設定',
|
||||
'authFiles.action.models': 'モデル',
|
||||
'authFiles.action.copy': 'コピー',
|
||||
};
|
||||
+15
-21
@@ -185,11 +185,11 @@ const CODEX_OAUTH_LOGIN_REQUIRED_ERROR = 'CODEX_OAUTH_LOGIN_REQUIRED';
|
||||
const DEFAULT_CLAUDE_CODE_MAX_CONTEXT_TOKENS = 200_000;
|
||||
const DEFAULT_CLAUDE_AUTO_COMPACT_PCT = 90;
|
||||
|
||||
const createClaudeModelMappings = (model: string): ClaudeModelMappings => ({
|
||||
opus: model,
|
||||
sonnet: model,
|
||||
haiku: model,
|
||||
fable: model,
|
||||
const createClaudeModelMappings = (): ClaudeModelMappings => ({
|
||||
opus: '',
|
||||
sonnet: '',
|
||||
haiku: '',
|
||||
fable: '',
|
||||
fable1m: false,
|
||||
opus1m: false,
|
||||
sonnet1m: false,
|
||||
@@ -207,8 +207,8 @@ const createClaudeModelMappingsByClient = (): Record<
|
||||
ClaudeModelMappingClientId,
|
||||
ClaudeModelMappings
|
||||
> => ({
|
||||
'claude-code': createClaudeModelMappings(''),
|
||||
'claude-desktop': { ...createClaudeModelMappings(''), desktopModels: createDefaultDesktopModels() },
|
||||
'claude-code': createClaudeModelMappings(),
|
||||
'claude-desktop': { ...createClaudeModelMappings(), desktopModels: createDefaultDesktopModels() },
|
||||
});
|
||||
|
||||
const createClaudeBooleanByClient = (): Record<ClaudeModelMappingClientId, boolean> => ({
|
||||
@@ -830,7 +830,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
const isClaudeModelMappingClient = selected === 'claude-code' || selected === 'claude-desktop';
|
||||
const claudeModelMappingsDraft = isClaudeModelMappingClient
|
||||
? claudeModelMappingsDraftByClient[selected]
|
||||
: createClaudeModelMappings('');
|
||||
: createClaudeModelMappings();
|
||||
const claudeCustomMapping = isClaudeModelMappingClient
|
||||
? claudeCustomMappingByClient[selected]
|
||||
: false;
|
||||
@@ -896,19 +896,19 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
current,
|
||||
selected,
|
||||
appliedMappings,
|
||||
createClaudeModelMappings(selected === 'claude-desktop' ? '' : selectedModel),
|
||||
createClaudeModelMappings(),
|
||||
dirty,
|
||||
);
|
||||
const next: ClaudeModelMappings = {
|
||||
...(selected === 'claude-desktop' ? { desktopModels: dirty && source.desktopModels
|
||||
? source.desktopModels : appliedMappings ? desktopModelEntries(source) : createDefaultDesktopModels() } : {}),
|
||||
opus: findAgentModel(models, source.opus)?.name ?? (selected === 'claude-code' ? source.opus || selectedModel : ''),
|
||||
sonnet: findAgentModel(models, source.sonnet)?.name ?? (selected === 'claude-code' ? source.sonnet || selectedModel : ''),
|
||||
haiku: findAgentModel(models, source.haiku)?.name ?? (selected === 'claude-code' ? source.haiku || selectedModel : ''),
|
||||
opus: findAgentModel(models, source.opus)?.name ?? '',
|
||||
sonnet: findAgentModel(models, source.sonnet)?.name ?? '',
|
||||
haiku: findAgentModel(models, source.haiku)?.name ?? '',
|
||||
opus1m: Boolean(source.opus1m),
|
||||
sonnet1m: Boolean(source.sonnet1m),
|
||||
haiku1m: Boolean(source.haiku1m),
|
||||
fable: source.fable || source.sonnet || selectedModel,
|
||||
fable: findAgentModel(models, source.fable)?.name ?? '',
|
||||
fable1m: Boolean(source.fable1m),
|
||||
maxContextTokens: source.maxContextTokens ?? DEFAULT_CLAUDE_CODE_MAX_CONTEXT_TOKENS,
|
||||
autoCompactPct: source.autoCompactPct ?? DEFAULT_CLAUDE_AUTO_COMPACT_PCT,
|
||||
@@ -928,7 +928,6 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
isClaudeModelMappingClient,
|
||||
models,
|
||||
selected,
|
||||
selectedModel,
|
||||
]);
|
||||
|
||||
const connectionState = activeStatus?.connectionState ?? 'invalid';
|
||||
@@ -1075,12 +1074,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
const model = findAgentModel(models, value);
|
||||
if (!model) return;
|
||||
if (isClaudeModelMappingClient) {
|
||||
editClaudeModelMappings((current) => ({
|
||||
...current,
|
||||
opus: model.name,
|
||||
sonnet: model.name,
|
||||
haiku: model.name,
|
||||
}));
|
||||
editClaudeModelMappings((current) => ({ ...current, sonnet: model.name }));
|
||||
}
|
||||
selectModel(model.name);
|
||||
};
|
||||
@@ -1164,7 +1158,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
return null;
|
||||
}
|
||||
const selectedEntries = selectedDesktopModelEntries(desktopEntries);
|
||||
return { ...createClaudeModelMappings(''), sonnet: selectedEntries[0].model.trim(),
|
||||
return { ...createClaudeModelMappings(), sonnet: selectedEntries[0].model.trim(),
|
||||
desktopModels: selectedEntries.map((entry) => ({ ...entry, model: entry.model.trim(), alias: entry.alias.trim() })) };
|
||||
}
|
||||
const resolved = {} as ClaudeModelMappings;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
.auth-files-heading-actions > .auth-toolbar-quiet { border-color: transparent; background: transparent; color: var(--auth-muted); }
|
||||
.auth-files-heading-actions > .auth-toolbar-quiet:hover:not(:disabled) { background: var(--bg-hover); color: var(--auth-text); }
|
||||
.auth-files-page .auth-files-panel {
|
||||
--auth-list-columns: minmax(168px, 1.1fr) 72px 92px 116px 156px minmax(240px, 2.4fr) 248px;
|
||||
--auth-list-columns: minmax(160px, 1fr) 72px 90px 112px 148px minmax(220px, 1.35fr) minmax(300px, 1.55fr);
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
border: 0;
|
||||
@@ -64,6 +64,7 @@
|
||||
.auth-status-badge.warning { color: var(--amber-text); background: color-mix(in srgb, var(--amber-text) 8%, var(--bg-card)); }
|
||||
.auth-status-badge.error { color: var(--failure-badge-text); background: color-mix(in srgb, var(--failure-badge-text) 7%, var(--bg-card)); }
|
||||
.auth-status-badge.info { color: var(--auth-accent); background: color-mix(in srgb, var(--auth-accent) 8%, var(--bg-card)); }
|
||||
.auth-status-badge.neutral { color: var(--auth-muted); background: color-mix(in srgb, var(--auth-muted) 10%, var(--bg-card)); }
|
||||
.auth-list-muted { color: var(--auth-muted); font-size: 12px; }
|
||||
.auth-list-recent .auth-file-requests { gap: 9px; }
|
||||
.auth-list-recent .auth-file-requests-latest { color: var(--auth-muted); font-size: 12px; font-weight: 650; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
@@ -80,14 +81,14 @@
|
||||
.auth-list-usage .auth-file-usage-metric.total svg { color: var(--auth-accent); background: color-mix(in srgb, var(--auth-accent) 9%, var(--bg-card)); }
|
||||
.auth-list-usage .auth-file-usage-metric strong { font-size: 13px; line-height: 1.2; font-weight: 750; }
|
||||
.auth-list-usage .auth-file-usage-scope { margin-top: 6px; color: var(--auth-muted); text-align: right; font-size: 10px; line-height: 1.2; }
|
||||
.auth-card-actions { display: flex; align-items: center; justify-content: flex-end; gap: 10px; min-width: 0; }
|
||||
.auth-list-icon-actions { display: flex; align-items: center; gap: 6px; }
|
||||
.auth-card-actions .auth-list-action { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 32px; min-width: 32px; height: 32px; padding: 0; border: 1px solid color-mix(in srgb, var(--auth-accent) 16%, var(--auth-border)); border-radius: 8px; color: var(--auth-accent); background: var(--bg-card); cursor: pointer; }
|
||||
.auth-card-actions { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 8px; min-width: 0; }
|
||||
.auth-list-icon-actions { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 6px; min-width: max-content; }
|
||||
.auth-card-actions .auth-list-action { display: inline-flex; align-items: center; justify-content: center; flex: none; width: auto; min-width: 0; min-height: 30px; padding: 4px 8px; gap: 4px; border: 1px solid color-mix(in srgb, var(--auth-accent) 16%, var(--auth-border)); border-radius: 8px; color: var(--auth-accent); background: var(--bg-card); font: inherit; font-size: 12px; font-weight: 650; line-height: 1.2; white-space: nowrap; cursor: pointer; }
|
||||
.auth-card-actions .auth-list-action.muted { color: var(--auth-text); border-color: var(--auth-border); background: var(--bg-card); }
|
||||
.auth-card-actions .auth-list-action.danger { color: #e11d48; border-color: color-mix(in srgb, #e11d48 22%, var(--auth-border)); background: var(--bg-card); }
|
||||
.auth-card-actions .auth-list-action:disabled { opacity: .38; cursor: default; }
|
||||
.auth-card-actions .auth-list-action:not(:disabled):hover { background: color-mix(in srgb, currentColor 8%, var(--bg-card)); border-color: currentColor; }
|
||||
.auth-list-main-actions { display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding-left: 10px; border-left: 1px solid var(--auth-border); }
|
||||
.auth-list-main-actions { display: flex; align-items: center; justify-content: flex-end; flex: none; gap: 8px; padding-left: 10px; border-left: 1px solid var(--auth-border); }
|
||||
.auth-list-switch { display: flex; align-items: center; flex: none; width: 40px; height: 24px; padding: 2px; border: 1px solid color-mix(in srgb, var(--auth-muted) 22%, var(--bg-card)); border-radius: 20px; background: color-mix(in srgb, var(--auth-muted) 28%, var(--bg-card)); cursor: pointer; }
|
||||
.auth-list-switch > span { display: block; width: 18px; height: 18px; flex-shrink: 0; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px #0002; transition: transform .15s; }
|
||||
.auth-list-switch[aria-checked="true"] { background: var(--auth-accent); border-color: var(--auth-accent); }
|
||||
@@ -153,16 +154,16 @@
|
||||
@media (prefers-reduced-motion: reduce) { .credential-quota-track.indeterminate i { animation: none; width: 100%; opacity: .3; } }
|
||||
|
||||
|
||||
@container credential-files (min-width: 701px) and (max-width: 1360px) {
|
||||
.auth-file-list-head, .auth-credential-row { grid-template-columns: minmax(132px, 1fr) 58px 74px 92px 128px minmax(168px, 1.6fr) 214px;; column-gap: 8px; }
|
||||
.auth-file-card-grid, .auth-file-list-head { min-width: 980px; }
|
||||
.auth-card-actions { gap: 8px; }
|
||||
.auth-list-icon-actions { gap: 4px; }
|
||||
.auth-card-actions .auth-list-action { width: 30px; min-width: 30px; height: 30px; }
|
||||
@container credential-files (min-width: 1501px) and (max-width: 1500px) {
|
||||
.auth-file-list-head, .auth-credential-row { grid-template-columns: minmax(136px, 1fr) 62px 78px 96px 128px minmax(170px, 1.25fr) minmax(220px, 1.15fr); column-gap: 8px; }
|
||||
.auth-file-card-grid, .auth-file-list-head { min-width: 1120px; }
|
||||
.auth-card-actions { display: grid; grid-template-columns: minmax(0, 1fr); justify-items: end; gap: 6px; }
|
||||
.auth-list-icon-actions { display: grid; grid-template-columns: repeat(3, max-content); justify-content: end; gap: 4px; }
|
||||
.auth-card-actions .auth-list-action { min-height: 28px; padding: 3px 7px; font-size: 12px; }
|
||||
.auth-list-details-button { min-width: 48px; min-height: 30px; padding-inline: 8px; font-size: 12px; }
|
||||
.auth-list-usage .auth-file-usage-metric strong { font-size: 12px; }
|
||||
}
|
||||
@container credential-files (max-width: 700px) {
|
||||
@container credential-files (max-width: 1500px) {
|
||||
.auth-file-table-scroll { overflow: visible; }
|
||||
.auth-file-list-head { display: none; }
|
||||
.auth-file-card-grid { min-width: 0; margin-top: 10px; }
|
||||
@@ -175,7 +176,7 @@
|
||||
.auth-list-quota { grid-column: 1 / -1; border-top: 1px solid var(--auth-border); padding-top: 12px; }
|
||||
.auth-card-actions { grid-column: 1 / -1; display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; border-top: 1px solid var(--auth-border); padding-top: 12px; }
|
||||
.auth-list-main-actions { display: flex; gap: 8px; padding-left: 8px; }
|
||||
.auth-list-icon-actions { display: flex; gap: 4px; }
|
||||
.auth-list-icon-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; }
|
||||
.auth-list-usage .auth-file-usage-metrics { gap: 5px; }
|
||||
.auth-list-usage .auth-file-usage-metric { gap: 3px; }
|
||||
.auth-list-usage .auth-file-usage-metric strong { font-size: 11px; }
|
||||
@@ -231,10 +232,30 @@
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.auth-files-page .credential-quota-dense .credential-quota-row:has(.credential-quota-row-detail) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.auth-files-page .credential-quota-dense .credential-quota-row-detail {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
.auth-files-page .auth-file-card {
|
||||
border-color: #edf0f4;
|
||||
box-shadow: 0 1px 2px rgb(35 48 65 / 3%);
|
||||
}
|
||||
.auth-files-page .auth-card-actions {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.auth-files-page .auth-list-icon-actions,
|
||||
.auth-files-page .auth-list-main-actions {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.auth-files-page .auth-file-card:hover {
|
||||
border-color: color-mix(in srgb, var(--auth-accent) 26%, #edf0f4);
|
||||
box-shadow: 0 4px 14px rgb(35 83 160 / 7%);
|
||||
|
||||
@@ -15,16 +15,13 @@ import {
|
||||
Check,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Copy,
|
||||
FileDown,
|
||||
FolderOpen,
|
||||
Import,
|
||||
LoaderCircle,
|
||||
Network,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Settings2,
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import antigravityIcon from '../assets/icons/antigravity.svg';
|
||||
@@ -576,11 +573,11 @@ export function AuthFileManagementPage() {
|
||||
</div>
|
||||
<footer className="auth-card-actions auth-list-cell" data-label={t('authFiles.list.actions')}>
|
||||
<div className="auth-list-icon-actions">
|
||||
<button type="button" className="auth-list-action" onClick={() => void refreshQuota(file)} disabled={busy || disabled || !quotaProvider || quota.status === 'loading'} title={t('authFiles.quota.refresh')} aria-label={t('authFiles.quota.refresh')}><RefreshCw size={15} className={quota.status === 'loading' ? 'spin' : ''} aria-hidden="true" /></button>
|
||||
<button type="button" className="auth-list-action" onClick={() => setSettingsName(name)} disabled={busy || resettingCooldown || !isOAuthCredentialFile(file)} title={t(isOAuthCredentialFile(file) ? 'authFiles.settings.title' : 'authFiles.fileOnly')} aria-label={t('authFiles.settings.title')}><Settings2 size={15} aria-hidden="true" /></button>
|
||||
<button type="button" className="auth-list-action" onClick={() => setModelViewName(name)} disabled={busy || !providerKey(file)} title={t('authFiles.models.viewTitle')} aria-label={t('authFiles.models.viewTitle')}><Network size={15} aria-hidden="true" /></button>
|
||||
<button type="button" className="auth-list-action muted" onClick={() => void copyName(name)} disabled={busy} title={t('authFiles.copyName')} aria-label={t('authFiles.copyName')}>{copied === name ? <Check size={15} aria-hidden="true" /> : <Copy size={15} aria-hidden="true" />}</button>
|
||||
<button type="button" className="auth-list-action danger" onClick={() => void deleteFile(file)} disabled={busy || resettingCooldown || isRuntimeOnly(file)} title={t('common.delete')} aria-label={t('common.delete')}><Trash2 size={15} aria-hidden="true" /></button>
|
||||
<button type="button" className="auth-list-action" onClick={() => void refreshQuota(file)} disabled={busy || disabled || !quotaProvider || quota.status === 'loading'} title={t('authFiles.quota.refresh')}><RefreshCw size={14} className={quota.status === 'loading' ? 'spin' : ''} aria-hidden="true" />{t('authFiles.action.refreshQuota')}</button>
|
||||
<button type="button" className="auth-list-action" onClick={() => setSettingsName(name)} disabled={busy || resettingCooldown || !isOAuthCredentialFile(file)} title={t(isOAuthCredentialFile(file) ? 'authFiles.settings.title' : 'authFiles.fileOnly')}>{t('authFiles.action.settings')}</button>
|
||||
<button type="button" className="auth-list-action" onClick={() => setModelViewName(name)} disabled={busy || !providerKey(file)} title={t('authFiles.models.viewTitle')}>{t('authFiles.action.models')}</button>
|
||||
<button type="button" className="auth-list-action muted" onClick={() => void copyName(name)} disabled={busy} title={t('authFiles.copyName')}>{copied === name ? <Check size={14} aria-hidden="true" /> : null}{t('authFiles.action.copy')}</button>
|
||||
<button type="button" className="auth-list-action danger" onClick={() => void deleteFile(file)} disabled={busy || resettingCooldown || isRuntimeOnly(file)} title={t('common.delete')}>{t('common.delete')}</button>
|
||||
</div>
|
||||
<div className="auth-list-main-actions">
|
||||
<button type="button" className="auth-list-switch" role="switch" aria-checked={!disabled} aria-label={`${t(disabled ? 'common.enable' : 'common.disable')} ${identity || name}`} onClick={() => void toggleStatus(file)} disabled={busy || resettingCooldown || !isOAuthCredentialFile(file)} title={t(isOAuthCredentialFile(file) ? disabled ? 'common.enable' : 'common.disable' : 'authFiles.fileOnly')}><span /></button>
|
||||
|
||||
@@ -3498,8 +3498,9 @@
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding-right: 4px;
|
||||
scrollbar-gutter: stable;
|
||||
padding-right: 0;
|
||||
scrollbar-gutter: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
@media (min-width: 761px) {
|
||||
|
||||
@@ -863,19 +863,26 @@ body, html, #root {
|
||||
}
|
||||
|
||||
/* Agent List & Config Scrollbar Polish */
|
||||
.agent-list-items::-webkit-scrollbar,
|
||||
.agent-config-scroll-region::-webkit-scrollbar {
|
||||
.agent-list-items::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.agent-list-items::-webkit-scrollbar-thumb,
|
||||
.agent-config-scroll-region::-webkit-scrollbar-thumb {
|
||||
.agent-config-scroll-region {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.agent-config-scroll-region::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.agent-list-items::-webkit-scrollbar-thumb {
|
||||
background: var(--border-primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.agent-list-items::-webkit-scrollbar-track,
|
||||
.agent-config-scroll-region::-webkit-scrollbar-track {
|
||||
.agent-list-items::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +58,9 @@ const fs = require('node:fs/promises');
|
||||
assert.equal(await card(1).getByRole('switch').isVisible(), true);
|
||||
assert.equal(await card(1).locator('.auth-list-details-button').isVisible(), true);
|
||||
assert.equal(await card(1).locator('.auth-list-details').isVisible(), false, `${label}: file metadata and the expanded request summary start collapsed`);
|
||||
if (width >= 1280) {
|
||||
if (width >= 1800) {
|
||||
const heights = await page.locator('.auth-credential-row').evaluateAll(nodes => nodes.map(node => node.getBoundingClientRect().height));
|
||||
assert.ok(heights.every(height => height >= 88 && height <= 120), `${label}: dense desktop rows stay near 104px, including quota footnotes: ${JSON.stringify(heights)}`);
|
||||
assert.ok(heights.every(height => height >= 88 && height <= 190), `${label}: desktop rows stay compact, allowing labeled actions to wrap: ${JSON.stringify(heights)}`);
|
||||
const cells = await card(1).locator('.auth-credential-row > .auth-list-cell').evaluateAll(nodes => nodes.map(node => {
|
||||
const bounds = node.getBoundingClientRect(); return { left: bounds.left, right: bounds.right };
|
||||
}));
|
||||
@@ -74,7 +74,7 @@ const fs = require('node:fs/promises');
|
||||
assert.equal(await previous().isDisabled(), true);
|
||||
assert.ok((await pagination().innerText()).includes('1 / 2'));
|
||||
assert.equal(await page.locator('.auth-file-list-head > span').count(), 7);
|
||||
await assertMainInformation('initial desktop', 1800);
|
||||
await assertMainInformation('initial desktop', 1500);
|
||||
assert.equal(await card(2).locator('.auth-file-health-compact').isVisible(), true, 'Cooldown summary is directly visible below the main row');
|
||||
assert.equal(await card(2).locator('.auth-health-body').isVisible(), false, 'Individual cooldown records start collapsed');
|
||||
assert.equal(await card(2).getByRole('button', { name: '清除冷却', exact: true }).isVisible(), true, 'Clear cooldown is available without opening file details');
|
||||
@@ -244,6 +244,35 @@ const fs = require('node:fs/promises');
|
||||
await page.locator('.auth-file-table-scroll').screenshot({ path: path.join(screenshotDir, 'reference-small-idle-preview.png') });
|
||||
assert.deepEqual(errors, []);
|
||||
assert.deepEqual(externalRequests, []);
|
||||
await page.goto(`${base}/tests/fixtures/auth-file-list.html?displayCases=1&locale=zh-CN&theme=light`);
|
||||
await cards().nth(5).waitFor();
|
||||
assert.equal(await cards().count(), 6, 'Display fixture renders every representative credential');
|
||||
const disabledBadges = page.locator('.auth-status-badge.neutral');
|
||||
assert.equal(await disabledBadges.count(), 5, 'Every disabled credential uses the neutral status treatment');
|
||||
for (const badge of await disabledBadges.all()) assert.equal(await badge.innerText(), '已停用');
|
||||
assert.equal(await page.locator('.auth-status-badge.success').innerText(), '可用');
|
||||
const actionLabels = ['刷新', '设置', '模型', '复制', '删除'];
|
||||
for (const label of actionLabels) {
|
||||
const action = cards().first().getByRole('button', { name: label, exact: true });
|
||||
assert.equal(await action.innerText(), label, `${label} is a visible text button`);
|
||||
assert.equal(await action.locator('svg title').count(), 0, `${label} does not rely on an icon-only label`);
|
||||
}
|
||||
assert.equal(await cards().last().locator('.credential-quota-row-detail').innerText(), '付费 API 对话可用。xAI 暂不为此 OAuth 凭证提供额度总量数据。');
|
||||
for (const width of [1680, 1366, 1180, 980, 390]) {
|
||||
await page.setViewportSize({ width, height: 1000 });
|
||||
const collisions = await page.locator('.auth-file-card').evaluateAll(nodes => nodes.flatMap(card => {
|
||||
const buttons = [...card.querySelectorAll('.auth-card-actions button')].filter(button => button.getClientRects().length);
|
||||
const boxes = buttons.map(button => button.getBoundingClientRect());
|
||||
return boxes.flatMap((box, index) => boxes.slice(index + 1)
|
||||
.filter(other => box.left < other.right - 1 && other.left < box.right - 1 && box.top < other.bottom - 1 && other.top < box.bottom - 1)
|
||||
.map(other => ({ width: innerWidth, texts: [buttons[index].textContent, buttons[boxes.indexOf(other)].textContent] })));
|
||||
}));
|
||||
assert.deepEqual(collisions, [], `${width}: credential actions overlap ${JSON.stringify(collisions)}`);
|
||||
const quotaOverflow = await page.locator('.auth-list-quota').evaluateAll(nodes => nodes.filter(node => node.scrollWidth > node.clientWidth + 1).map(node => node.textContent));
|
||||
assert.deepEqual(quotaOverflow, [], `${width}: quota content overflows its column`);
|
||||
await page.screenshot({ path: path.join(screenshotDir, `display-cases-${width}.png`), fullPage: true });
|
||||
}
|
||||
|
||||
console.log('PASS: offline seven-column dense credential list with visible request/runtime/quota/action data, compact row height, exact quota percentages/reset timestamps, unknown quota, ten/two pagination, search/provider/status reset, page size twenty, additional quota disclosure, main-row and detail-summary request keyboard tooltips, directly visible cooldown controls, exact status PATCH, direct delete confirmation/cancel, small idle accounts without pagination, light/dark Chinese/English 1800/1280/1024/390 without page or card overflow, contained table scrolling only at intermediate widths.');
|
||||
console.log(`Screenshots: ${screenshotDir}`);
|
||||
} finally { await browser?.close(); await server.close(); }
|
||||
|
||||
@@ -15,7 +15,7 @@ const os = require('node:os');
|
||||
assert.equal(await startup.inputValue(), 'sonnet');
|
||||
await page.locator('.claude-role-row .agent-model-trigger').first().click();
|
||||
await page.getByRole('option', { name: /gpt-two/ }).click();
|
||||
await page.getByText('sonnet → gpt-two', { exact: true }).waitFor();
|
||||
assert.equal(await page.locator('.claude-code-model-summary').count(), 0);
|
||||
assert.equal(await startup.inputValue(), 'sonnet');
|
||||
await startup.click();
|
||||
await page.getByRole('option', { name: /^gpt-two / }).click();
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// Run Vite on port 1421, or set AGENT_MODEL_LAYOUT_TEST_URL to another local server.
|
||||
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const base = process.env.AGENT_MODEL_LAYOUT_TEST_URL || 'http://127.0.0.1:1421';
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--no-proxy-server'] });
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
page.setDefaultTimeout(10000);
|
||||
const errors = [];
|
||||
page.on('pageerror', error => errors.push(String(error)));
|
||||
for (const locale of ['zh-CN', 'en', 'ja']) {
|
||||
for (const theme of ['light', 'dark']) {
|
||||
for (const width of [1392, 1100, 760, 480]) {
|
||||
await page.setViewportSize({ width, height: 962 });
|
||||
await page.goto(`${base}/tests/fixtures/agent-backups.html?client=claude-code&reset-selections&shell&claude-alias-layout&locale=${locale}&theme=${theme}`);
|
||||
await page.locator('.claude-role-row[data-role=sonnet] .agent-model-trigger small').waitFor();
|
||||
await page.evaluate(() => document.fonts.ready);
|
||||
const issues = await page.locator('.claude-role-table').evaluate(table => {
|
||||
const issues = [];
|
||||
for (const row of table.querySelectorAll('.claude-role-row')) {
|
||||
const trigger = row.querySelector('.agent-model-trigger');
|
||||
const bounds = trigger.getBoundingClientRect();
|
||||
for (const text of trigger.querySelectorAll('strong, small')) {
|
||||
const rect = text.getBoundingClientRect();
|
||||
if (rect.top < bounds.top - 1 || rect.bottom > bounds.bottom + 1
|
||||
|| rect.left < bounds.left - 1 || rect.right > bounds.right + 1) {
|
||||
issues.push(`${row.dataset.role}: model text exceeds the selector`);
|
||||
}
|
||||
}
|
||||
const next = row.nextElementSibling?.getBoundingClientRect();
|
||||
if (next && bounds.bottom > next.top + 1) issues.push(`${row.dataset.role}: selector overlaps the next row`);
|
||||
if (row.scrollWidth > row.clientWidth + 1) issues.push(`${row.dataset.role}: horizontal overflow`);
|
||||
const toggle = row.querySelector('.claude-inline-context').getBoundingClientRect();
|
||||
if (bounds.right > toggle.left + 1) issues.push(`${row.dataset.role}: selector overlaps the context switch`);
|
||||
}
|
||||
return issues;
|
||||
});
|
||||
assert.deepEqual(issues, [], `${locale}/${theme}/${width}`);
|
||||
if (locale === 'zh-CN' && theme === 'light' && [1392, 480].includes(width)) {
|
||||
await page.screenshot({ path: path.join(os.tmpdir(), `claude-code-model-layout-${width}.png`), fullPage: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await page.setViewportSize({ width: 1392, height: 962 });
|
||||
await page.goto(`${base}/tests/fixtures/agent-backups.html?client=claude-code&reset-selections&shell&claude-alias-layout`);
|
||||
const sonnet = page.locator('.claude-role-row[data-role=sonnet]');
|
||||
await sonnet.locator('.agent-model-trigger small').waitFor();
|
||||
await page.locator('.agent-claude-desktop-mapping-filter input').check();
|
||||
const trigger = sonnet.locator('.agent-model-trigger');
|
||||
await trigger.click();
|
||||
await page.getByRole('option', { name: /^gpt-6\.1-sol / }).click();
|
||||
assert.equal(await trigger.locator('small').count(), 0);
|
||||
await trigger.focus();
|
||||
await trigger.press('Enter');
|
||||
await sonnet.locator('.agent-model-search input').fill('high-fast');
|
||||
await page.getByRole('option', { name: /^gpt-6.1-sol-high-fast / }).click();
|
||||
assert.equal(await trigger.locator('small').innerText(), 'gpt-6.1-sol');
|
||||
await page.getByRole('switch', { name: 'sonnet 1M', exact: true }).check();
|
||||
await page.getByRole('button', { name: '更新配置', exact: true }).click();
|
||||
await page.waitForFunction(() => window.fixtureCalls.some(call => call.cmd === 'update_agent_config'
|
||||
&& call.args.claudeCodeModelMappings.sonnet === 'gpt-6.1-sol-high-fast'
|
||||
&& call.args.claudeCodeModelMappings.sonnet1m === true));
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS: 24 viewport/language/theme layouts, alias selection, keyboard search, context toggle and configuration update');
|
||||
} finally { await browser.close(); }
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
Vendored
+7
@@ -13,6 +13,12 @@ const ids = ['claude-code','claude-desktop','codex','opencode','openclaw','herme
|
||||
let count=0; let backupCount=0; const backups:any[]=[]; let currentModel=params.has('fresh')?null:'gpt-one';
|
||||
let nativeOauth=params.has('native-oauth'); let nativeSwitchCount=0; let codexClosed=false; let closeCount=0;
|
||||
let currentOauth=false; const currentMappings:Record<string,any>={};
|
||||
const claudeAliasLayoutModels = [
|
||||
{name:'gpt-6.1-sol-high-fast',alias:'gpt-6.1-sol',isAlias:true},
|
||||
{name:'gpt-6.1-sol',isAlias:true},
|
||||
{name:'provider/very-long-upstream-model-name-with-reasoning-high',alias:'provider/very-long-original-model-name-with-context-window',isAlias:true},
|
||||
];
|
||||
if(params.has('claude-alias-layout'))currentMappings['claude-code']={opus:claudeAliasLayoutModels[0].name,sonnet:claudeAliasLayoutModels[0].name,fable:claudeAliasLayoutModels[1].name,haiku:claudeAliasLayoutModels[2].name,startupModel:'opus',subagentModel:''};
|
||||
if(params.has('legacy-desktop'))currentMappings['claude-desktop']={opus:'gpt-one',sonnet:'gpt-two',haiku:'gpt-one',opus1m:true};
|
||||
if(params.has('legacy-desktop-direct'))currentMappings['claude-desktop']={opus:'',sonnet:'claude-sonnet-custom-7',haiku:'',desktopModels:[{model:'',alias:'claude-sonnet-custom-7',context1m:false}]};
|
||||
if(params.has('saved-desktop-alias'))currentMappings['claude-desktop']={opus:'',sonnet:'gpt-one',haiku:'',desktopModels:[{model:'gpt-one',alias:'claude-opus-5',context1m:false}]};
|
||||
@@ -65,6 +71,7 @@ mockIPC(async (cmd,args:any) => {
|
||||
nativeOauth=true; return {outcome:'updated'};
|
||||
}
|
||||
if(cmd==='get_agent_models') {
|
||||
if(params.has('claude-alias-layout'))return claudeAliasLayoutModels;
|
||||
if(params.has('no-models'))return [];
|
||||
if(params.has('no-core'))throw new Error('CPA core is offline');
|
||||
if(params.has('defer-models'))await new Promise<void>(resolve=>{(window as any).fixtureFinishModels=resolve;});
|
||||
|
||||
Vendored
+14
-9
@@ -14,15 +14,20 @@ document.documentElement.dataset.theme = params.get('theme') || 'light';
|
||||
const startedAt = Date.now();
|
||||
const smallList = params.has('small');
|
||||
type Request = { path: string; method: string; body?: Record<string, unknown>; query?: Record<string, string> };
|
||||
const files: Record<string, unknown>[] = Array.from({ length: smallList ? 3 : 12 }, (_, index) => ({
|
||||
name: `${String(index + 1).padStart(2, '0')}-account.json`,
|
||||
email: index === 0 ? 'long-fictional-account-for-layout@example.test' : `account-${index + 1}@example.test`,
|
||||
auth_index: `fixture-account-${index + 1}`, provider: smallList ? ['codex', 'devin', 'xai'][index] : index === 3 ? 'claude' : index === 4 ? 'xai' : 'codex',
|
||||
const displayCases = params.has('displayCases');
|
||||
const files: Record<string, unknown>[] = Array.from({ length: displayCases ? 6 : smallList ? 3 : 12 }, (_, index) => ({
|
||||
name: displayCases
|
||||
? ['antigravity-very-long-account-name-for-layout.json', 'codex-team-subscription.json', 'codex-free-disabled.json', 'devin-user-layout-check.json', 'kimi-layout-check.json', 'xai-paid-account.json'][index]
|
||||
: `${String(index + 1).padStart(2, '0')}-account.json`,
|
||||
email: displayCases
|
||||
? ['sasaki245668-layout-check@example.test', '1154606263-layout-check@example.test', 'lzt404rum-codex-layout@example.test', 'lzt404rum-devin-layout@example.test', 'kimi-layout-check@example.test', 'lori-doyle-layout-check@example.test'][index]
|
||||
: index === 0 ? 'long-fictional-account-for-layout@example.test' : `account-${index + 1}@example.test`,
|
||||
auth_index: `fixture-account-${index + 1}`, provider: displayCases ? ['antigravity', 'codex', 'codex', 'devin', 'kimi', 'xai'][index] : smallList ? ['codex', 'devin', 'xai'][index] : index === 3 ? 'claude' : index === 4 ? 'xai' : 'codex',
|
||||
...(params.has('longQuota') ? { provider: 'antigravity' } : {}),
|
||||
source: 'file', size: 1800 + index * 350, updated_at: '2026-10-01T00:00:00Z',
|
||||
disabled: smallList ? index !== 1 : index === 2 || index === 6,
|
||||
status: (smallList ? index !== 1 : index === 2 || index === 6) ? 'disabled' : 'active',
|
||||
priority: smallList ? 0 : index % 3, plan_type: smallList ? index === 0 ? 'Team' : '' : index % 3 === 0 ? 'Pro 20x' : 'Team',
|
||||
disabled: displayCases ? index < 5 : smallList ? index !== 1 : index === 2 || index === 6,
|
||||
status: (displayCases ? index < 5 : smallList ? index !== 1 : index === 2 || index === 6) ? 'disabled' : 'active',
|
||||
priority: displayCases || smallList ? 0 : index % 3, plan_type: displayCases ? ['', 'Team', 'Free', '', '', 'Paid'][index] : smallList ? index === 0 ? 'Team' : '' : index % 3 === 0 ? 'Pro 20x' : 'Team',
|
||||
note: index === 0 ? 'Fictional account note, visible after opening credential details.' : '',
|
||||
success: smallList ? 0 : 1200 + index * 2450, failed: smallList ? 0 : 3 + index * 97,
|
||||
recent_requests: Array.from({ length: smallList ? 0 : 20 }, (_, bucket) => ({
|
||||
@@ -35,13 +40,13 @@ const files: Record<string, unknown>[] = Array.from({ length: smallList ? 3 : 12
|
||||
}));
|
||||
|
||||
updateQuotaCache(Object.fromEntries(files.map((file, index) => {
|
||||
if (smallList) return [quotaKey(file), { status: 'idle', rows: [] } satisfies QuotaState];
|
||||
if (smallList || (displayCases && index < 5)) return [quotaKey(file), { status: 'idle', rows: [] } satisfies QuotaState];
|
||||
const quota: QuotaState = {
|
||||
status: 'success', plan: String(file.plan_type),
|
||||
subscriptionActiveUntil: index === 0 ? '2026-11-01T00:00:00Z' : undefined,
|
||||
resetCredits: index === 0 ? 2 : undefined,
|
||||
resetCreditsApplicable: index === 0 ? 0 : undefined,
|
||||
rows: index === 4 ? [{ label: 'Paid API account', remainingPercent: null, detail: 'Upstream does not report remaining quota.' }]
|
||||
rows: displayCases || index === 4 ? [{ label: '付费 API 账号', remainingPercent: null, detail: '付费 API 对话可用。xAI 暂不为此 OAuth 凭证提供额度总量数据。' }]
|
||||
: [
|
||||
{ label: '5h', remainingPercent: index === 1 ? 0 : index === 2 ? 8 : 72 - index * 3, resetAtMs: startedAt + 3 * 3600000 },
|
||||
{ label: 'Weekly', remainingPercent: 83 - index * 5, resetAtMs: startedAt + 5 * 86400000 },
|
||||
|
||||
Reference in new issue
Block a user