Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 71 additions & 3 deletions packages/cli/src/ui/components/StatusLineDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -117,10 +138,10 @@ describe('StatusLineDialog', () => {
useThemeColors: true,
items: [
'model-with-reasoning',
'git-branch',
'context-remaining',
'current-dir',
'context-used',
'git-branch',
],
});
expect(
Expand All @@ -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(
<KeypressProvider kittyProtocolEnabled={false}>
<StatusLineDialog
settings={settings}
config={config}
uiState={uiState}
addItem={vi.fn()}
onClose={vi.fn()}
availableTerminalHeight={18}
/>
</KeypressProvider>,
);

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 = {
Expand Down
16 changes: 5 additions & 11 deletions packages/cli/src/ui/components/StatusLineDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
buildStatusLinePresetLines,
DEFAULT_STATUS_LINE_PRESET_CONFIG,
normalizeStatusLinePresetConfig,
orderStatusLinePresetItems,
STATUS_LINE_PRESET_ITEMS,
type StatusLinePresetConfig,
type StatusLinePresetItemId,
Expand Down Expand Up @@ -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),
};
}

Expand Down Expand Up @@ -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,
Expand Down
30 changes: 29 additions & 1 deletion packages/cli/src/ui/hooks/useStatusLine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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({
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/ui/hooks/useStatusLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading