mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
fix(ui): display model name instead of id in statusline and startup banner (#4741)
* fix(ui): display model name instead of id in statusline and startup banner * Update packages/cli/src/ui/components/ModelDialog.tsx Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> * fix(ui): remove duplicate model.id display in ModelDialog * test(core): add unit tests for ModelsConfig/Config.getModelDisplayName() * refactor(core): simplify Config.getModelDisplayName() by removing dead 'unknown' branch --------- Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
This commit is contained in:
parent
871b9c1eb7
commit
6d083fad5d
11 changed files with 175 additions and 19 deletions
|
|
@ -47,6 +47,7 @@ const createSettings = (options?: {
|
|||
const createMockConfig = (overrides = {}) => ({
|
||||
getContentGeneratorConfig: vi.fn(() => ({ authType: undefined })),
|
||||
getModel: vi.fn(() => 'gemini-pro'),
|
||||
getModelDisplayName: vi.fn(() => 'Gemini Pro'),
|
||||
getTargetDir: vi.fn(() => '/projects/qwen-code'),
|
||||
getMcpServers: vi.fn(() => ({})),
|
||||
getBlockedMcpServers: vi.fn(() => []),
|
||||
|
|
@ -106,7 +107,7 @@ describe('<AppHeader />', () => {
|
|||
it('shows the header with all info when banner is visible', () => {
|
||||
const { lastFrame } = renderWithProviders(createMockUIState());
|
||||
expect(lastFrame()).toContain('>_ Qwen Code');
|
||||
expect(lastFrame()).toContain('gemini-pro');
|
||||
expect(lastFrame()).toContain('Gemini Pro');
|
||||
expect(lastFrame()).toContain('/projects/qwen-code');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import { Header, AuthDisplayType } from './Header.js';
|
|||
import { Tips } from './Tips.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { resolveCustomBanner } from '../utils/customBanner.js';
|
||||
|
||||
interface AppHeaderProps {
|
||||
|
|
@ -50,11 +49,9 @@ function getAuthDisplayType(
|
|||
export const AppHeader = ({ version }: AppHeaderProps) => {
|
||||
const settings = useSettings();
|
||||
const config = useConfig();
|
||||
const uiState = useUIState();
|
||||
|
||||
const contentGeneratorConfig = config.getContentGeneratorConfig();
|
||||
const authType = contentGeneratorConfig?.authType;
|
||||
const model = uiState.currentModel;
|
||||
const model = config.getModelDisplayName();
|
||||
const targetDir = config.getTargetDir();
|
||||
const showBanner =
|
||||
!config.getScreenReader() && !settings.merged.ui?.hideBanner;
|
||||
|
|
|
|||
|
|
@ -290,6 +290,12 @@ export function ModelDialog({
|
|||
[{t2}]
|
||||
</Text>
|
||||
<Text>{` ${model.label}`}</Text>
|
||||
{model.id !== model.label && (
|
||||
<Text color={theme.text.secondary} italic>
|
||||
{' '}
|
||||
({model.id})
|
||||
</Text>
|
||||
)}
|
||||
{isRuntime && (
|
||||
<Text color={theme.status.warning}> (Runtime)</Text>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ function createSettings(): LoadedSettings {
|
|||
const config = {
|
||||
getCliVersion: () => '1.2.3',
|
||||
getModel: () => 'qwen3-code-plus',
|
||||
getModelDisplayName: () => 'Qwen3 Code Plus',
|
||||
getTargetDir: () => '/repo/project',
|
||||
getContentGeneratorConfig: () => ({
|
||||
contextWindowSize: 1000,
|
||||
|
|
@ -106,7 +107,7 @@ describe('StatusLineDialog', () => {
|
|||
frame.indexOf('current-dir'),
|
||||
);
|
||||
expect(lastFrame()).toContain('Preview');
|
||||
expect(lastFrame()).toContain('qwen3-code-plus high');
|
||||
expect(lastFrame()).toContain('Qwen3 Code Plus high');
|
||||
});
|
||||
|
||||
it('persists selected presets on enter', async () => {
|
||||
|
|
@ -187,7 +188,7 @@ describe('StatusLineDialog', () => {
|
|||
await press(' ');
|
||||
|
||||
expect(lastFrame()).toContain(
|
||||
'qwen3-code-plus high | feature/pr-4087-statusline | Context 75% left',
|
||||
'Qwen3 Code Plus high | feature/pr-4087-statusline | Context 75% left',
|
||||
);
|
||||
|
||||
await press('\r');
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ function getPreviewData(config: Config, uiState: UIState) {
|
|||
return buildStatusLinePresetData({
|
||||
sessionId: stats.sessionId,
|
||||
version: config.getCliVersion(),
|
||||
modelDisplayName: uiState.currentModel || config.getModel(),
|
||||
modelDisplayName: config.getModelDisplayName(),
|
||||
reasoning: contentGeneratorConfig?.reasoning,
|
||||
currentDir: config.getTargetDir(),
|
||||
branch: uiState.branchName,
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ const getMockContentGeneratorConfig = (): MockContentGeneratorConfig => ({
|
|||
const mockConfig = {
|
||||
getTargetDir: vi.fn(() => '/test/dir'),
|
||||
getModel: vi.fn(() => 'test-model'),
|
||||
getModelDisplayName: vi.fn(() => 'Test Model'),
|
||||
getCliVersion: vi.fn(() => '1.0.0'),
|
||||
getContentGeneratorConfig: vi.fn(getMockContentGeneratorConfig),
|
||||
};
|
||||
|
|
@ -282,7 +283,7 @@ describe('useStatusLine', () => {
|
|||
const { result } = renderHook(() => useStatusLine());
|
||||
|
||||
expect(result.current.useThemeColors).toBe(true);
|
||||
expect(result.current.lines).toEqual(['test-model']);
|
||||
expect(result.current.lines).toEqual(['Test Model']);
|
||||
});
|
||||
|
||||
it('looks up the current branch pull request number with gh', async () => {
|
||||
|
|
@ -315,7 +316,7 @@ describe('useStatusLine', () => {
|
|||
const { result } = renderHook(() => useStatusLine());
|
||||
|
||||
expect(child_process.exec).not.toHaveBeenCalled();
|
||||
expect(result.current.lines).toEqual(['test-model']);
|
||||
expect(result.current.lines).toEqual(['Test Model']);
|
||||
});
|
||||
|
||||
it('renders model-with-reasoning and model-only together', () => {
|
||||
|
|
@ -330,7 +331,7 @@ describe('useStatusLine', () => {
|
|||
const { result } = renderHook(() => useStatusLine());
|
||||
|
||||
expect(child_process.exec).not.toHaveBeenCalled();
|
||||
expect(result.current.lines).toEqual(['test-model high | test-model']);
|
||||
expect(result.current.lines).toEqual(['Test Model high | Test Model']);
|
||||
});
|
||||
|
||||
it('refreshes when status line settings are saved in the same process', async () => {
|
||||
|
|
@ -342,7 +343,7 @@ describe('useStatusLine', () => {
|
|||
const { result, rerender } = renderHook(() => useStatusLine());
|
||||
|
||||
expect(child_process.exec).not.toHaveBeenCalled();
|
||||
expect(result.current.lines).toEqual(['test-model']);
|
||||
expect(result.current.lines).toEqual(['Test Model']);
|
||||
|
||||
setStatusLineConfig({
|
||||
type: 'preset',
|
||||
|
|
@ -365,7 +366,7 @@ describe('useStatusLine', () => {
|
|||
vi.advanceTimersByTime(300);
|
||||
});
|
||||
|
||||
expect(result.current.lines).toEqual(['test-model | #4118']);
|
||||
expect(result.current.lines).toEqual(['Test Model | #4118']);
|
||||
});
|
||||
|
||||
it('reloads status line settings from disk when streaming becomes idle', async () => {
|
||||
|
|
@ -375,7 +376,7 @@ describe('useStatusLine', () => {
|
|||
});
|
||||
const { result, rerender } = renderHook(() => useStatusLine());
|
||||
|
||||
expect(result.current.lines).toEqual(['test-model']);
|
||||
expect(result.current.lines).toEqual(['Test Model']);
|
||||
|
||||
mockSettings.reloadScopeFromDisk.mockImplementationOnce(() => {
|
||||
setStatusLineConfig({
|
||||
|
|
@ -397,7 +398,7 @@ describe('useStatusLine', () => {
|
|||
});
|
||||
|
||||
expect(mockSettings.reloadScopeFromDisk).toHaveBeenCalledOnce();
|
||||
expect(result.current.lines).toEqual(['test-model high']);
|
||||
expect(result.current.lines).toEqual(['Test Model high']);
|
||||
});
|
||||
|
||||
it('uses command settings when a stale preset override no longer matches the settings type', () => {
|
||||
|
|
@ -582,7 +583,7 @@ describe('useStatusLine', () => {
|
|||
const input = JSON.parse(stdinWrittenData);
|
||||
expect(input.session_id).toBe('test-session');
|
||||
expect(input.version).toBe('1.0.0');
|
||||
expect(input.model.display_name).toBe('test-model');
|
||||
expect(input.model.display_name).toBe('Test Model');
|
||||
expect(input.workspace.current_dir).toBe('/test/dir');
|
||||
});
|
||||
|
||||
|
|
@ -687,7 +688,7 @@ describe('useStatusLine', () => {
|
|||
renderHook(() => useStatusLine());
|
||||
|
||||
const input = JSON.parse(stdinWrittenData);
|
||||
expect(input.model.display_name).toBe('test-model');
|
||||
expect(input.model.display_name).toBe('Test Model');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -393,7 +393,7 @@ export function useStatusLine(): {
|
|||
const data = buildStatusLinePresetData({
|
||||
sessionId: stats.sessionId,
|
||||
version: cfg.getCliVersion(),
|
||||
modelDisplayName: ui.currentModel || cfg.getModel(),
|
||||
modelDisplayName: cfg.getModelDisplayName(),
|
||||
reasoning: contentGeneratorConfig?.reasoning,
|
||||
currentDir,
|
||||
branch: ui.branchName,
|
||||
|
|
@ -444,7 +444,7 @@ export function useStatusLine(): {
|
|||
session_id: stats.sessionId,
|
||||
version: cfg.getCliVersion() || 'unknown',
|
||||
model: {
|
||||
display_name: ui.currentModel || cfg.getModel() || 'unknown',
|
||||
display_name: cfg.getModelDisplayName(),
|
||||
},
|
||||
context_window: {
|
||||
context_window_size: contextWindowSize,
|
||||
|
|
|
|||
|
|
@ -3561,4 +3561,58 @@ describe('Model Switching and Config Updates', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModelDisplayName', () => {
|
||||
it('should return resolved name when model is in registry', () => {
|
||||
const config = new Config({
|
||||
...baseParams,
|
||||
authType: AuthType.USE_OPENAI,
|
||||
model: 'gpt-4o',
|
||||
modelProvidersConfig: {
|
||||
[AuthType.USE_OPENAI]: [
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
name: 'GPT-4o',
|
||||
baseUrl: 'https://api.openai.example.com/v1',
|
||||
envKey: 'OPENAI_API_KEY',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(config.getModelDisplayName()).toBe('GPT-4o');
|
||||
});
|
||||
|
||||
it('should return raw modelId when model is not in registry', () => {
|
||||
const config = new Config({
|
||||
...baseParams,
|
||||
authType: AuthType.USE_OPENAI,
|
||||
model: 'custom-runtime-model',
|
||||
modelProvidersConfig: {
|
||||
[AuthType.USE_OPENAI]: [
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
name: 'GPT-4o',
|
||||
baseUrl: 'https://api.openai.example.com/v1',
|
||||
envKey: 'OPENAI_API_KEY',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(config.getModelDisplayName()).toBe('custom-runtime-model');
|
||||
});
|
||||
|
||||
it('should return raw modelId when currentAuthType is falsy', () => {
|
||||
const config = new Config({
|
||||
...baseParams,
|
||||
model: 'some-model',
|
||||
// authType is not set
|
||||
});
|
||||
|
||||
// getModel() returns 'some-model', getModelDisplayName returns it as-is
|
||||
// because currentAuthType is falsy
|
||||
expect(config.getModelDisplayName()).toBe('some-model');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2199,6 +2199,15 @@ export class Config {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the human-readable display name for the currently selected model.
|
||||
* Resolves the model id to its name from the model registry.
|
||||
* Falls back to the raw model id when the model is not found.
|
||||
*/
|
||||
getModelDisplayName(): string {
|
||||
return this.modelsConfig.getModelDisplayName(this.getModel());
|
||||
}
|
||||
|
||||
onModelChange(listener: (model: string) => void): () => void {
|
||||
this.modelChangeListeners.add(listener);
|
||||
return () => {
|
||||
|
|
|
|||
|
|
@ -2421,4 +2421,79 @@ describe('ModelsConfig', () => {
|
|||
expect(gc.samplingParams).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getModelDisplayName', () => {
|
||||
it('should return resolved.name when model is found in registry', () => {
|
||||
const modelProvidersConfig: ModelProvidersConfig = {
|
||||
openai: [
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
name: 'GPT-4o',
|
||||
baseUrl: 'https://api.openai.example.com/v1',
|
||||
envKey: 'OPENAI_API_KEY',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const modelsConfig = new ModelsConfig({
|
||||
initialAuthType: AuthType.USE_OPENAI,
|
||||
modelProvidersConfig,
|
||||
});
|
||||
|
||||
expect(modelsConfig.getModelDisplayName('gpt-4o')).toBe('GPT-4o');
|
||||
});
|
||||
|
||||
it('should return raw modelId when currentAuthType is falsy', () => {
|
||||
const modelsConfig = new ModelsConfig();
|
||||
// currentAuthType is undefined by default
|
||||
|
||||
expect(modelsConfig.getModelDisplayName('some-model')).toBe('some-model');
|
||||
});
|
||||
|
||||
it('should return raw modelId when model is not found in registry', () => {
|
||||
const modelProvidersConfig: ModelProvidersConfig = {
|
||||
openai: [
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
name: 'GPT-4o',
|
||||
baseUrl: 'https://api.openai.example.com/v1',
|
||||
envKey: 'OPENAI_API_KEY',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const modelsConfig = new ModelsConfig({
|
||||
initialAuthType: AuthType.USE_OPENAI,
|
||||
modelProvidersConfig,
|
||||
});
|
||||
|
||||
// 'unknown-model' is not in the registry
|
||||
expect(modelsConfig.getModelDisplayName('unknown-model')).toBe(
|
||||
'unknown-model',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return raw modelId when model.name equals model.id', () => {
|
||||
const modelProvidersConfig: ModelProvidersConfig = {
|
||||
openai: [
|
||||
{
|
||||
id: 'coder-model',
|
||||
name: 'coder-model',
|
||||
baseUrl: 'https://api.openai.example.com/v1',
|
||||
envKey: 'OPENAI_API_KEY',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const modelsConfig = new ModelsConfig({
|
||||
initialAuthType: AuthType.USE_OPENAI,
|
||||
modelProvidersConfig,
|
||||
});
|
||||
|
||||
// name === id, so registry returns the id as name
|
||||
expect(modelsConfig.getModelDisplayName('coder-model')).toBe(
|
||||
'coder-model',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -312,6 +312,18 @@ export class ModelsConfig {
|
|||
return this.modelRegistry.getModel(authType, modelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display name for a model by its id.
|
||||
* Looks up the model in the registry using the current authType and returns
|
||||
* its resolved name. Falls back to the raw model id when the model is not
|
||||
* found in the registry (e.g. runtime models or unknown models).
|
||||
*/
|
||||
getModelDisplayName(modelId: string): string {
|
||||
if (!this.currentAuthType) return modelId;
|
||||
const resolved = this.modelRegistry.getModel(this.currentAuthType, modelId);
|
||||
return resolved?.name ?? modelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set model programmatically (e.g., VLM auto-switch, fallback).
|
||||
* Supports both registry models and raw model IDs.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue