diff --git a/packages/cli/src/ui/components/StatusLineDialog.test.tsx b/packages/cli/src/ui/components/StatusLineDialog.test.tsx
index 8364d1e4510..3f3f601115d 100644
--- a/packages/cli/src/ui/components/StatusLineDialog.test.tsx
+++ b/packages/cli/src/ui/components/StatusLineDialog.test.tsx
@@ -49,7 +49,10 @@ const config = {
getCliVersion: () => '1.2.3',
getModel: () => 'qwen3-code-plus',
getTargetDir: () => '/repo/project',
- getContentGeneratorConfig: () => ({ contextWindowSize: 1000 }),
+ getContentGeneratorConfig: () => ({
+ contextWindowSize: 1000,
+ reasoning: { effort: 'high' },
+ }),
} as Config;
const uiState = {
@@ -84,8 +87,26 @@ describe('StatusLineDialog', () => {
expect(lastFrame()).toContain('Configure Status Line');
expect(lastFrame()).toContain('Type to search');
+ const frame = lastFrame() ?? '';
+ expect(frame).toContain('model-with-reasoning');
+ expect(frame).toContain('model-only');
+ expect(frame).toContain('git-branch');
+ expect(frame).toContain('context-remaining');
+ expect(frame).toContain('current-dir');
+ expect(frame.indexOf('model-with-reasoning')).toBeLessThan(
+ frame.indexOf('model-only'),
+ );
+ expect(frame.indexOf('model-only')).toBeLessThan(
+ frame.indexOf('git-branch'),
+ );
+ expect(frame.indexOf('git-branch')).toBeLessThan(
+ frame.indexOf('context-remaining'),
+ );
+ expect(frame.indexOf('context-remaining')).toBeLessThan(
+ frame.indexOf('current-dir'),
+ );
expect(lastFrame()).toContain('Preview');
- expect(lastFrame()).toContain('qwen3-code-plus');
+ expect(lastFrame()).toContain('qwen3-code-plus high');
});
it('persists selected presets on enter', async () => {
@@ -117,10 +138,10 @@ describe('StatusLineDialog', () => {
useThemeColors: true,
items: [
'model-with-reasoning',
+ 'git-branch',
'context-remaining',
'current-dir',
'context-used',
- 'git-branch',
],
});
expect(
@@ -137,6 +158,53 @@ describe('StatusLineDialog', () => {
expect(onClose).toHaveBeenCalled();
});
+ it('keeps preset priority order after an item is toggled off and on', async () => {
+ const settings = createSettings();
+ const { stdin, lastFrame } = render(
+
+
+ ,
+ );
+
+ const press = async (input: string) => {
+ act(() => {
+ stdin.write(input);
+ });
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ };
+
+ await press('j');
+ await press('j');
+ await press('j');
+ await press(' ');
+ await press(' ');
+
+ expect(lastFrame()).toContain(
+ 'qwen3-code-plus high | feature/pr-4087-statusline | Context 75% left',
+ );
+
+ await press('\r');
+
+ expect(settings.merged.ui?.statusLine).toEqual({
+ type: 'preset',
+ useThemeColors: true,
+ items: [
+ 'model-with-reasoning',
+ 'git-branch',
+ 'context-remaining',
+ 'current-dir',
+ 'context-used',
+ ],
+ });
+ });
+
it('saves back to workspace settings when workspace config is effective', async () => {
const settings = createSettings();
settings.workspace.settings.ui = {
diff --git a/packages/cli/src/ui/components/StatusLineDialog.tsx b/packages/cli/src/ui/components/StatusLineDialog.tsx
index 92637dffdbc..6368d1bc7c2 100644
--- a/packages/cli/src/ui/components/StatusLineDialog.tsx
+++ b/packages/cli/src/ui/components/StatusLineDialog.tsx
@@ -22,6 +22,7 @@ import {
buildStatusLinePresetLines,
DEFAULT_STATUS_LINE_PRESET_CONFIG,
normalizeStatusLinePresetConfig,
+ orderStatusLinePresetItems,
STATUS_LINE_PRESET_ITEMS,
type StatusLinePresetConfig,
type StatusLinePresetItemId,
@@ -57,19 +58,11 @@ function buildInitialSelectedKeys(settings: LoadedSettings): string[] {
function buildConfigFromKeys(keys: readonly string[]): StatusLinePresetConfig {
const selected = new Set(keys);
- const validItemIds = new Set(STATUS_LINE_PRESET_ITEMS.map((item) => item.id));
- const items = [
- ...new Set(
- keys.filter((key): key is StatusLinePresetItemId =>
- validItemIds.has(key as StatusLinePresetItemId),
- ),
- ),
- ];
return {
type: 'preset',
useThemeColors: selected.has(THEME_COLORS_KEY),
- items,
+ items: orderStatusLinePresetItems(keys),
};
}
@@ -102,15 +95,16 @@ function getPreviewData(config: Config, uiState: UIState) {
const stats = uiState.sessionStats;
const metrics = stats.metrics;
const { totalInputTokens, totalOutputTokens } = aggregateModelTokens(metrics);
+ const contentGeneratorConfig = config.getContentGeneratorConfig();
return buildStatusLinePresetData({
sessionId: stats.sessionId,
version: config.getCliVersion(),
modelDisplayName: uiState.currentModel || config.getModel(),
+ reasoning: contentGeneratorConfig?.reasoning,
currentDir: config.getTargetDir(),
branch: uiState.branchName,
- contextWindowSize:
- config.getContentGeneratorConfig()?.contextWindowSize || 0,
+ contextWindowSize: contentGeneratorConfig?.contextWindowSize || 0,
currentUsage: stats.lastPromptTokenCount,
totalInputTokens,
totalOutputTokens,
diff --git a/packages/cli/src/ui/hooks/useStatusLine.test.ts b/packages/cli/src/ui/hooks/useStatusLine.test.ts
index fafe1908cf7..daf61874874 100644
--- a/packages/cli/src/ui/hooks/useStatusLine.test.ts
+++ b/packages/cli/src/ui/hooks/useStatusLine.test.ts
@@ -8,6 +8,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import * as child_process from 'child_process';
import { StreamingState } from '../types.js';
+import type { StatusLinePresetReasoning } from '../statusLinePresets.js';
const debugLogMock = vi.hoisted(() => ({
log: vi.fn(),
@@ -49,11 +50,20 @@ vi.mock('../contexts/UIStateContext.js', () => ({
useUIState: () => mockUIState,
}));
+type MockContentGeneratorConfig = {
+ contextWindowSize: number;
+ reasoning?: StatusLinePresetReasoning;
+};
+
+const getMockContentGeneratorConfig = (): MockContentGeneratorConfig => ({
+ contextWindowSize: 131072,
+});
+
const mockConfig = {
getTargetDir: vi.fn(() => '/test/dir'),
getModel: vi.fn(() => 'test-model'),
getCliVersion: vi.fn(() => '1.0.0'),
- getContentGeneratorConfig: vi.fn(() => ({ contextWindowSize: 131072 })),
+ getContentGeneratorConfig: vi.fn(getMockContentGeneratorConfig),
};
vi.mock('../contexts/ConfigContext.js', () => ({
useConfig: () => mockConfig,
@@ -149,6 +159,9 @@ describe('useStatusLine', () => {
mockUIState.sessionStats.metrics.files.totalLinesRemoved = 0;
mockVimMode.vimEnabled = false;
mockVimMode.vimMode = 'INSERT';
+ mockConfig.getContentGeneratorConfig.mockReturnValue({
+ contextWindowSize: 131072,
+ });
// Dynamic import to get fresh module after mocks
const mod = await import('./useStatusLine.js');
@@ -236,6 +249,21 @@ describe('useStatusLine', () => {
expect(result.current.lines).toEqual(['test-model']);
});
+ it('renders model-with-reasoning and model-only together', () => {
+ mockConfig.getContentGeneratorConfig.mockReturnValue({
+ contextWindowSize: 131072,
+ reasoning: { effort: 'high' },
+ });
+ setStatusLineConfig({
+ type: 'preset',
+ items: ['model', 'model-with-reasoning'],
+ });
+ const { result } = renderHook(() => useStatusLine());
+
+ expect(child_process.exec).not.toHaveBeenCalled();
+ expect(result.current.lines).toEqual(['test-model high | test-model']);
+ });
+
it('refreshes when status line settings are saved in the same process', async () => {
mockUIState.branchName = 'dragon/feat-reproduce-skill';
setStatusLineConfig({
diff --git a/packages/cli/src/ui/hooks/useStatusLine.ts b/packages/cli/src/ui/hooks/useStatusLine.ts
index 26b533d9da3..20fb7c9dec4 100644
--- a/packages/cli/src/ui/hooks/useStatusLine.ts
+++ b/packages/cli/src/ui/hooks/useStatusLine.ts
@@ -373,12 +373,13 @@ export function useStatusLine(): {
const { totalInputTokens, totalOutputTokens } = aggregateModelTokens(m);
- const contextWindowSize =
- cfg.getContentGeneratorConfig()?.contextWindowSize || 0;
+ const contentGeneratorConfig = cfg.getContentGeneratorConfig();
+ const contextWindowSize = contentGeneratorConfig?.contextWindowSize || 0;
const data = buildStatusLinePresetData({
sessionId: stats.sessionId,
version: cfg.getCliVersion(),
modelDisplayName: ui.currentModel || cfg.getModel(),
+ reasoning: contentGeneratorConfig?.reasoning,
currentDir,
branch: ui.branchName,
pullRequestNumber: pullRequestNumberRef.current,
diff --git a/packages/cli/src/ui/statusLinePresets.test.ts b/packages/cli/src/ui/statusLinePresets.test.ts
index 4e6178dc697..6a1a8b32264 100644
--- a/packages/cli/src/ui/statusLinePresets.test.ts
+++ b/packages/cli/src/ui/statusLinePresets.test.ts
@@ -11,15 +11,18 @@ import {
buildStatusLinePresetData,
buildStatusLinePresetLines,
DEFAULT_STATUS_LINE_PRESET_CONFIG,
+ formatModelWithReasoning,
formatTokenCount,
getRunStateLabel,
inferPullRequestNumber,
normalizeStatusLinePresetConfig,
+ orderStatusLinePresetItems,
STATUS_LINE_PRESET_ITEM_IDS,
+ STATUS_LINE_PRESET_ITEMS,
} from './statusLinePresets.js';
describe('statusLinePresets', () => {
- it('normalizes valid preset configs and drops unknown items', () => {
+ it('normalizes valid preset configs and orders items by priority', () => {
expect(
normalizeStatusLinePresetConfig({
type: 'preset',
@@ -54,7 +57,51 @@ describe('statusLinePresets', () => {
).toEqual(DEFAULT_STATUS_LINE_PRESET_CONFIG);
});
- it('renders available preset items and omits unavailable optional fields', () => {
+ it('keeps default preset items in priority order', () => {
+ expect(DEFAULT_STATUS_LINE_PRESET_CONFIG.items).toEqual(
+ orderStatusLinePresetItems(
+ [...DEFAULT_STATUS_LINE_PRESET_CONFIG.items].reverse(),
+ ),
+ );
+ });
+
+ it('orders preset items directly', () => {
+ expect(orderStatusLinePresetItems([])).toEqual([]);
+ expect(orderStatusLinePresetItems(['bogus'])).toEqual([]);
+ expect(orderStatusLinePresetItems([42, null])).toEqual([]);
+ expect(
+ orderStatusLinePresetItems([
+ 'run-state',
+ 'model',
+ 'git-branch',
+ 'model',
+ 'context-remaining',
+ ]),
+ ).toEqual(['model', 'git-branch', 'context-remaining', 'run-state']);
+ });
+
+ it('formats model reasoning directly', () => {
+ expect(formatModelWithReasoning('qwen3-code-plus', false)).toBe(
+ 'qwen3-code-plus reasoning off',
+ );
+ expect(
+ formatModelWithReasoning('qwen3-code-plus', { effort: 'high' }),
+ ).toBe('qwen3-code-plus high');
+ expect(
+ formatModelWithReasoning('qwen3-code-plus', { effort: undefined }),
+ ).toBe('qwen3-code-plus');
+ expect(formatModelWithReasoning('qwen3-code-plus', undefined)).toBe(
+ 'qwen3-code-plus',
+ );
+ });
+
+ it('labels the plain model preset as model-only', () => {
+ expect(
+ STATUS_LINE_PRESET_ITEMS.find((item) => item.id === 'model')?.label,
+ ).toBe('model-only');
+ });
+
+ it('renders available preset items in priority order', () => {
const data = buildStatusLinePresetData({
sessionId: 'session-123',
version: '1.2.3',
@@ -75,12 +122,12 @@ describe('statusLinePresets', () => {
{
type: 'preset',
items: [
+ 'run-state',
'model',
- 'context-remaining',
- 'current-dir',
- 'pull-request-number',
'branch-changes',
- 'run-state',
+ 'pull-request-number',
+ 'current-dir',
+ 'context-remaining',
],
},
data,
@@ -95,6 +142,7 @@ describe('statusLinePresets', () => {
sessionId: 'session-123',
version: '1.2.3',
modelDisplayName: 'qwen3-code-plus',
+ reasoning: { effort: 'high' },
currentDir: '/repo/project',
branch: 'feature/pr-4087-statusline',
contextWindowSize: 1000,
@@ -115,11 +163,67 @@ describe('statusLinePresets', () => {
data,
),
).toEqual([
- 'qwen3-code-plus | Context 75% left | /repo/project | Context 25% used | feature/pr-4087-statusline | project | #4087 | +12 -3 | Ready | v1.2.3 | 1.0k window | 250 used | 1.2k in | 340 out | session-123',
+ 'qwen3-code-plus high | qwen3-code-plus | feature/pr-4087-statusline | Context 75% left | 1.2k in | 340 out | /repo/project | project | #4087 | +12 -3 | Context 25% used | Ready | v1.2.3 | 1.0k window | 250 used | session-123',
]);
});
- it('treats model and model-with-reasoning as mutually exclusive', () => {
+ it('renders model and model-with-reasoning together', () => {
+ const data = buildStatusLinePresetData({
+ sessionId: 'session-123',
+ version: '1.2.3',
+ modelDisplayName: 'qwen3-code-plus',
+ reasoning: { effort: 'high' },
+ currentDir: '/repo/project',
+ branch: undefined,
+ contextWindowSize: 0,
+ currentUsage: 0,
+ totalInputTokens: 0,
+ totalOutputTokens: 0,
+ totalLinesAdded: 0,
+ totalLinesRemoved: 0,
+ streamingState: StreamingState.Idle,
+ });
+
+ expect(
+ buildStatusLinePresetLines(
+ {
+ type: 'preset',
+ items: ['model', 'model-with-reasoning'],
+ },
+ data,
+ ),
+ ).toEqual(['qwen3-code-plus high | qwen3-code-plus']);
+ });
+
+ it('shows when reasoning is disabled', () => {
+ const data = buildStatusLinePresetData({
+ sessionId: 'session-123',
+ version: '1.2.3',
+ modelDisplayName: 'qwen3-code-plus',
+ reasoning: false,
+ currentDir: '/repo/project',
+ branch: undefined,
+ contextWindowSize: 0,
+ currentUsage: 0,
+ totalInputTokens: 0,
+ totalOutputTokens: 0,
+ totalLinesAdded: 0,
+ totalLinesRemoved: 0,
+ streamingState: StreamingState.Idle,
+ });
+
+ expect(
+ buildStatusLinePresetLines(
+ {
+ type: 'preset',
+ items: ['model-with-reasoning'],
+ },
+ data,
+ ),
+ ).toEqual(['qwen3-code-plus reasoning off']);
+ });
+
+ it('falls back to the model name when reasoning is unset', () => {
const data = buildStatusLinePresetData({
sessionId: 'session-123',
version: '1.2.3',
@@ -139,7 +243,7 @@ describe('statusLinePresets', () => {
buildStatusLinePresetLines(
{
type: 'preset',
- items: ['model-with-reasoning', 'model'],
+ items: ['model-with-reasoning'],
},
data,
),
diff --git a/packages/cli/src/ui/statusLinePresets.ts b/packages/cli/src/ui/statusLinePresets.ts
index 4b548dd7f3f..d82b20c8713 100644
--- a/packages/cli/src/ui/statusLinePresets.ts
+++ b/packages/cli/src/ui/statusLinePresets.ts
@@ -9,20 +9,20 @@ import { StreamingState } from './types.js';
export const STATUS_LINE_PRESET_ITEM_IDS = [
'model-with-reasoning',
+ 'model',
+ 'git-branch',
'context-remaining',
+ 'total-input-tokens',
+ 'total-output-tokens',
'current-dir',
- 'context-used',
- 'git-branch',
- 'model',
'project-name',
'pull-request-number',
'branch-changes',
+ 'context-used',
'run-state',
'qwen-version',
'context-window-size',
'used-tokens',
- 'total-input-tokens',
- 'total-output-tokens',
'session-id',
] as const;
@@ -42,10 +42,18 @@ export interface StatusLinePresetConfig {
useThemeColors?: boolean;
}
+export type StatusLinePresetReasoning =
+ | false
+ | {
+ effort?: 'low' | 'medium' | 'high' | 'max';
+ }
+ | undefined;
+
export interface StatusLinePresetData {
sessionId: string;
version: string;
modelDisplayName: string;
+ reasoning: StatusLinePresetReasoning;
currentDir: string;
projectName: string | undefined;
branch: string | undefined;
@@ -80,6 +88,17 @@ export const STATUS_LINE_PRESET_ITEMS: readonly StatusLinePresetItem[] = [
description: 'Current model name with reasoning level when available',
defaultSelected: true,
},
+ {
+ id: 'model',
+ label: 'model-only',
+ description: 'Current model name without reasoning level',
+ },
+ {
+ id: 'git-branch',
+ label: 'git-branch',
+ description: 'Current Git branch when available',
+ defaultSelected: true,
+ },
{
id: 'context-remaining',
label: 'context-remaining',
@@ -87,28 +106,21 @@ export const STATUS_LINE_PRESET_ITEMS: readonly StatusLinePresetItem[] = [
defaultSelected: true,
},
{
- id: 'current-dir',
- label: 'current-dir',
- description: 'Current working directory',
- defaultSelected: true,
+ id: 'total-input-tokens',
+ label: 'total-input-tokens',
+ description: 'Total input tokens used in session',
},
{
- id: 'context-used',
- label: 'context-used',
- description: 'Percentage of context window used',
- defaultSelected: true,
+ id: 'total-output-tokens',
+ label: 'total-output-tokens',
+ description: 'Total output tokens used in session',
},
{
- id: 'git-branch',
- label: 'git-branch',
- description: 'Current Git branch when available',
+ id: 'current-dir',
+ label: 'current-dir',
+ description: 'Current working directory',
defaultSelected: true,
},
- {
- id: 'model',
- label: 'model',
- description: 'Current model name',
- },
{
id: 'project-name',
label: 'project-name',
@@ -124,6 +136,12 @@ export const STATUS_LINE_PRESET_ITEMS: readonly StatusLinePresetItem[] = [
label: 'branch-changes',
description: 'Session file changes added and removed',
},
+ {
+ id: 'context-used',
+ label: 'context-used',
+ description: 'Percentage of context window used',
+ defaultSelected: true,
+ },
{
id: 'run-state',
label: 'run-state',
@@ -144,16 +162,6 @@ export const STATUS_LINE_PRESET_ITEMS: readonly StatusLinePresetItem[] = [
label: 'used-tokens',
description: 'Current prompt tokens used',
},
- {
- id: 'total-input-tokens',
- label: 'total-input-tokens',
- description: 'Total input tokens used in session',
- },
- {
- id: 'total-output-tokens',
- label: 'total-output-tokens',
- description: 'Total output tokens used in session',
- },
{
id: 'session-id',
label: 'session-id',
@@ -165,11 +173,26 @@ const STATUS_LINE_PRESET_ITEM_ID_SET = new Set(
STATUS_LINE_PRESET_ITEM_IDS,
);
+export function orderStatusLinePresetItems(
+ items: readonly unknown[],
+): StatusLinePresetItemId[] {
+ const selectedItems = new Set(
+ items.filter(
+ (item): item is StatusLinePresetItemId =>
+ typeof item === 'string' && STATUS_LINE_PRESET_ITEM_ID_SET.has(item),
+ ),
+ );
+
+ return STATUS_LINE_PRESET_ITEM_IDS.filter((item) => selectedItems.has(item));
+}
+
export const DEFAULT_STATUS_LINE_PRESET_CONFIG: StatusLinePresetConfig = {
type: 'preset',
useThemeColors: true,
- items: STATUS_LINE_PRESET_ITEMS.filter((item) => item.defaultSelected).map(
- (item) => item.id,
+ items: orderStatusLinePresetItems(
+ STATUS_LINE_PRESET_ITEMS.filter((item) => item.defaultSelected).map(
+ (item) => item.id,
+ ),
),
};
@@ -186,12 +209,8 @@ export function normalizeStatusLinePresetConfig(
}
const hasItemsArray = Array.isArray(candidate['items']);
- const rawItems = hasItemsArray ? (candidate['items'] as unknown[]) : [];
const items = hasItemsArray
- ? rawItems.filter(
- (item): item is StatusLinePresetItemId =>
- typeof item === 'string' && STATUS_LINE_PRESET_ITEM_ID_SET.has(item),
- )
+ ? orderStatusLinePresetItems(candidate['items'] as unknown[])
: [];
return {
@@ -200,9 +219,7 @@ export function normalizeStatusLinePresetConfig(
typeof candidate['useThemeColors'] === 'boolean'
? candidate['useThemeColors']
: true,
- items: hasItemsArray
- ? [...new Set(items)]
- : [...DEFAULT_STATUS_LINE_PRESET_CONFIG.items],
+ items: hasItemsArray ? items : [...DEFAULT_STATUS_LINE_PRESET_CONFIG.items],
};
}
@@ -240,6 +257,19 @@ export function getRunStateLabel(state: StreamingState): string {
}
}
+export function formatModelWithReasoning(
+ modelDisplayName: string,
+ reasoning: StatusLinePresetReasoning,
+): string {
+ if (reasoning === false) {
+ return `${modelDisplayName} reasoning off`;
+ }
+ if (reasoning?.effort) {
+ return `${modelDisplayName} ${reasoning.effort}`;
+ }
+ return modelDisplayName;
+}
+
export function inferPullRequestNumber(
branch: string | undefined,
): string | undefined {
@@ -256,6 +286,7 @@ export function buildStatusLinePresetData(params: {
sessionId: string;
version: string | undefined;
modelDisplayName: string | undefined;
+ reasoning?: StatusLinePresetReasoning;
currentDir: string;
branch: string | undefined;
pullRequestNumber?: string | undefined;
@@ -284,6 +315,7 @@ export function buildStatusLinePresetData(params: {
sessionId: params.sessionId,
version: params.version || 'unknown',
modelDisplayName: params.modelDisplayName || 'unknown',
+ reasoning: params.reasoning,
currentDir: params.currentDir,
projectName: nodePath.basename(params.currentDir) || undefined,
branch: params.branch,
@@ -305,20 +337,16 @@ export function buildStatusLinePresetParts(
data: StatusLinePresetData,
): string[] {
const parts: string[] = [];
- const seen = new Set();
-
- for (const item of config.items) {
- if (seen.has(item)) {
- continue;
- }
- seen.add(item);
+ for (const item of orderStatusLinePresetItems(config.items)) {
switch (item) {
case 'model-with-reasoning':
+ parts.push(
+ formatModelWithReasoning(data.modelDisplayName, data.reasoning),
+ );
+ break;
case 'model':
parts.push(data.modelDisplayName);
- seen.add('model');
- seen.add('model-with-reasoning');
break;
case 'context-remaining':
if (data.contextWindowSize > 0) {