diff --git a/packages/cli/src/ui/components/AppHeader.test.tsx b/packages/cli/src/ui/components/AppHeader.test.tsx
index 392d9f74b0..cb15252a27 100644
--- a/packages/cli/src/ui/components/AppHeader.test.tsx
+++ b/packages/cli/src/ui/components/AppHeader.test.tsx
@@ -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('', () => {
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');
});
diff --git a/packages/cli/src/ui/components/AppHeader.tsx b/packages/cli/src/ui/components/AppHeader.tsx
index 2bff0766b1..b8662dc37a 100644
--- a/packages/cli/src/ui/components/AppHeader.tsx
+++ b/packages/cli/src/ui/components/AppHeader.tsx
@@ -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;
diff --git a/packages/cli/src/ui/components/ModelDialog.tsx b/packages/cli/src/ui/components/ModelDialog.tsx
index afb3ce8606..f8f9e17a16 100644
--- a/packages/cli/src/ui/components/ModelDialog.tsx
+++ b/packages/cli/src/ui/components/ModelDialog.tsx
@@ -290,6 +290,12 @@ export function ModelDialog({
[{t2}]
{` ${model.label}`}
+ {model.id !== model.label && (
+
+ {' '}
+ ({model.id})
+
+ )}
{isRuntime && (
(Runtime)
)}
diff --git a/packages/cli/src/ui/components/StatusLineDialog.test.tsx b/packages/cli/src/ui/components/StatusLineDialog.test.tsx
index 3f3f601115..391e1d9b2c 100644
--- a/packages/cli/src/ui/components/StatusLineDialog.test.tsx
+++ b/packages/cli/src/ui/components/StatusLineDialog.test.tsx
@@ -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');
diff --git a/packages/cli/src/ui/components/StatusLineDialog.tsx b/packages/cli/src/ui/components/StatusLineDialog.tsx
index 6368d1bc7c..62aa9c7d89 100644
--- a/packages/cli/src/ui/components/StatusLineDialog.tsx
+++ b/packages/cli/src/ui/components/StatusLineDialog.tsx
@@ -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,
diff --git a/packages/cli/src/ui/hooks/useStatusLine.test.ts b/packages/cli/src/ui/hooks/useStatusLine.test.ts
index c2b851b8a4..4918010712 100644
--- a/packages/cli/src/ui/hooks/useStatusLine.test.ts
+++ b/packages/cli/src/ui/hooks/useStatusLine.test.ts
@@ -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');
});
});
diff --git a/packages/cli/src/ui/hooks/useStatusLine.ts b/packages/cli/src/ui/hooks/useStatusLine.ts
index 504923be10..0d522c1f88 100644
--- a/packages/cli/src/ui/hooks/useStatusLine.ts
+++ b/packages/cli/src/ui/hooks/useStatusLine.ts
@@ -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,
diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts
index 1feb4947bf..3121856ef6 100644
--- a/packages/core/src/config/config.test.ts
+++ b/packages/core/src/config/config.test.ts
@@ -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');
+ });
+ });
});
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index f7c8f7c731..9e5f1a02ff 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -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 () => {
diff --git a/packages/core/src/models/modelsConfig.test.ts b/packages/core/src/models/modelsConfig.test.ts
index 937472543d..8801aa44b4 100644
--- a/packages/core/src/models/modelsConfig.test.ts
+++ b/packages/core/src/models/modelsConfig.test.ts
@@ -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',
+ );
+ });
+ });
});
diff --git a/packages/core/src/models/modelsConfig.ts b/packages/core/src/models/modelsConfig.ts
index c10143888f..43caae852f 100644
--- a/packages/core/src/models/modelsConfig.ts
+++ b/packages/core/src/models/modelsConfig.ts
@@ -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.