Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2837be6
fix(cli): prevent dialog clipping in short terminals
shenyankm Aug 13, 2026
1ba8a96
fix(cli): address dialog height review feedback
shenyankm Aug 13, 2026
ae05ca6
test(cli): pin dialog height budget branches and document locked-skil…
shenyankm Aug 13, 2026
477fab8
fix(cli): make dialog height budgets width-aware
shenyankm Aug 13, 2026
46c9d09
fix(cli): harden dialog height budgets
shenyankm Aug 14, 2026
ba35a4c
fix(cli): shed dialog chrome below the height-budget floor
shenyankm Aug 14, 2026
42303a5
test(cli): pin round-5 budget fixes
shenyankm Aug 14, 2026
07b371d
fix(cli): translate the locked-skills subtitle badge
shenyankm Aug 14, 2026
2cf2dd5
fix(cli): shed skills dialog chrome to the bare list at tiny budgets
shenyankm Aug 14, 2026
4d98703
fix(cli): simplify short skills dialog handling
shenyankm Aug 14, 2026
44f20a4
test(cli): pin review round-9 suggestions for dialog height fix
shenyankm Aug 14, 2026
a77698e
fix(cli): address dialog height review feedback
shenyankm Aug 15, 2026
d1590f1
fix(cli): preserve dialog search and label behavior
shenyankm Aug 15, 2026
7e2ce19
chore(cli): merge main into dialog-height fix, reconciling skills dialog
qwen-code-dev-bot Sep 1, 2026
3ca6695
test(cli): restore skills dialog config mock
shenyankm Sep 2, 2026
30908bb
Merge branch 'main' into fix/dialog-height-overflow
wenshao Sep 4, 2026
68ea6ff
Merge branch 'main' into fix/dialog-height-overflow
wenshao Sep 6, 2026
e4cbcad
chore: merge upstream main into dialog height fix
shenyankm Sep 12, 2026
1643d93
fix(cli): preserve locked skill search within height budgets
shenyankm Sep 12, 2026
0cd5121
docs: record PR 9040 review and verification follow-ups
shenyankm Sep 12, 2026
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
2 changes: 2 additions & 0 deletions docs/users/features/status-line.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ The easiest way to configure a status line is the `/statusline` command. It open

This opens the preset mode configurator. Use arrow keys to navigate, space to toggle items, and enter to confirm. Your selection is saved to settings automatically.

When fewer than 16 rows are available, the dialog switches to a compact list-only layout without search or preview.

You can also give `/statusline` specific instructions to have it generate a command-mode configuration:

```
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/ca.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ export default {
'No skills match the search.': 'Cap habilitat coincideix amb la cerca.',
'Locked by higher-scope settings (cannot toggle here):':
"Bloquejades per paràmetres d'àmbit superior (aquí no es poden commutar):",
'Hidden locked skills: {{count}} · type to search':
'Habilitats bloquejades ocultes: {{count}} · escriu per cercar',
'higher scope': 'àmbit superior',
' {{name}} {{description}} [locked: {{scope}}]':
' {{name}} {{description}} [bloquejada: {{scope}}]',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export default {
'No skills match the search.': 'Keine Skills passen zur Suche.',
'Locked by higher-scope settings (cannot toggle here):':
'Gesperrt durch Einstellungen einer höheren Ebene (kann hier nicht umgeschaltet werden):',
'Hidden locked skills: {{count}} · type to search':
'Ausgeblendete gesperrte Skills: {{count}} · zum Suchen tippen',
'higher scope': 'höhere Ebene',
' {{name}} {{description}} [locked: {{scope}}]':
' {{name}} {{description}} [gesperrt: {{scope}}]',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ export default {
'No skills match the search.': 'No skills match the search.',
'Locked by higher-scope settings (cannot toggle here):':
'Locked by higher-scope settings (cannot toggle here):',
'Hidden locked skills: {{count}} · type to search':
'Hidden locked skills: {{count}} · type to search',
'higher scope': 'higher scope',
' {{name}} {{description}} [locked: {{scope}}]':
' {{name}} {{description}} [locked: {{scope}}]',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ export default {
'Aucune compétence ne correspond à la recherche.',
'Locked by higher-scope settings (cannot toggle here):':
'Verrouillées par des paramètres de portée supérieure (impossible de basculer ici) :',
'Hidden locked skills: {{count}} · type to search':
'Compétences verrouillées masquées : {{count}} · saisissez pour rechercher',
'higher scope': 'portée supérieure',
' {{name}} {{description}} [locked: {{scope}}]':
' {{name}} {{description}} [verrouillée : {{scope}}]',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/ja.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export default {
'No skills match the search.': '検索に一致するスキルはありません。',
'Locked by higher-scope settings (cannot toggle here):':
'上位スコープ設定によってロックされています(ここでは切替不可):',
'Hidden locked skills: {{count}} · type to search':
'非表示のロック済みスキル: {{count}} · 入力して検索',
'higher scope': '上位スコープ',
' {{name}} {{description}} [locked: {{scope}}]':
' {{name}} {{description}} [ロック中:{{scope}}]',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/pt.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ export default {
'No skills match the search.': 'Nenhuma habilidade corresponde à pesquisa.',
'Locked by higher-scope settings (cannot toggle here):':
'Bloqueado por configurações de escopo superior (não é possível alternar aqui):',
'Hidden locked skills: {{count}} · type to search':
'Habilidades bloqueadas ocultas: {{count}} · digite para pesquisar',
'higher scope': 'escopo superior',
' {{name}} {{description}} [locked: {{scope}}]':
' {{name}} {{description}} [bloqueado: {{scope}}]',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/ru.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ export default {
'No skills match the search.': 'Нет навыков, соответствующих поиску.',
'Locked by higher-scope settings (cannot toggle here):':
'Заблокированы настройками более высокого уровня (здесь переключить нельзя):',
'Hidden locked skills: {{count}} · type to search':
'Скрытые заблокированные навыки: {{count}} · введите текст для поиска',
'higher scope': 'более высокий уровень',
' {{name}} {{description}} [locked: {{scope}}]':
' {{name}} {{description}} [заблокировано: {{scope}}]',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@ export default {
'No skills match the search.': '沒有符合搜尋條件的技能。',
'Locked by higher-scope settings (cannot toggle here):':
'被更高範圍設定鎖定(此處無法切換):',
'Hidden locked skills: {{count}} · type to search':
'已隱藏 {{count}} 個鎖定技能 · 輸入以搜尋',
'higher scope': '更高範圍',
' {{name}} {{description}} [locked: {{scope}}]':
' {{name}} {{description}} [已鎖定:{{scope}}]',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ export default {
'No skills match the search.': '没有匹配搜索的技能。',
'Locked by higher-scope settings (cannot toggle here):':
'被更高范围设置锁定(此处无法切换):',
'Hidden locked skills: {{count}} · type to search':
'已隐藏 {{count}} 个锁定技能 · 输入以搜索',
'higher scope': '更高范围',
' {{name}} {{description}} [locked: {{scope}}]':
' {{name}} {{description}} [已锁定:{{scope}}]',
Expand Down
77 changes: 76 additions & 1 deletion packages/cli/src/ui/components/StatusLineDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('StatusLineDialog', () => {
uiState={uiState}
addItem={vi.fn()}
onClose={vi.fn()}
availableTerminalHeight={18}
availableTerminalHeight={25}
/>
</KeypressProvider>,
);
Expand Down Expand Up @@ -376,4 +376,79 @@ describe('StatusLineDialog', () => {
expect(lastFrame()).not.toContain('> mj');
expect(lastFrame()).not.toContain('> mk');
});

it.each([
[16, true],
[15, false],
] as const)(
'uses the expected layout at the %i-row boundary',
(availableTerminalHeight, hasFullLayout) => {
const { lastFrame } = render(
<KeypressProvider kittyProtocolEnabled={false}>
<StatusLineDialog
settings={createSettings()}
config={config}
uiState={uiState}
addItem={vi.fn()}
onClose={vi.fn()}
availableTerminalHeight={availableTerminalHeight}
/>
</KeypressProvider>,
);

const frame = lastFrame() ?? '';
expect(frame.split('\n').length).toBeLessThanOrEqual(
availableTerminalHeight,
);
expect(frame.includes('Configure Status Line')).toBe(hasFullLayout);
},
);

it('uses every available row in an intermediate compact layout', () => {
const { lastFrame } = render(
<KeypressProvider kittyProtocolEnabled={false}>
<StatusLineDialog
settings={createSettings()}
config={config}
uiState={uiState}
addItem={vi.fn()}
onClose={vi.fn()}
availableTerminalHeight={5}
/>
</KeypressProvider>,
);

expect(lastFrame()?.split('\n').length).toBeLessThanOrEqual(5);
expect(lastFrame()).toContain('model-with-reasoning');
});

it('keeps every option reachable in a one-line layout', 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={1}
/>
</KeypressProvider>,
);

expect(lastFrame()?.split('\n').length).toBeLessThanOrEqual(1);
expect(lastFrame()).not.toContain('Configure Status Line');

// 8 j presses: theme-colors -> (skip separator) -> ... -> current-dir.
for (let i = 0; i < 8; i++) {
Comment thread
shenyankm marked this conversation as resolved.
Resolved
act(() => {
stdin.write('j');
});
await new Promise((resolve) => setTimeout(resolve, 0));
}
expect(lastFrame()).toContain('current-dir');
// The active item must stay inside the visible window.
expect(lastFrame()).toMatch(/›.*current-dir/);
});
});
109 changes: 65 additions & 44 deletions packages/cli/src/ui/components/StatusLineDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ interface StatusLineDialogProps {

const THEME_COLORS_KEY = 'theme-colors';
const DESCRIPTION_COLUMN = 24;
const STATUS_LINE_DIALOG_FIXED_ROWS = 15;

function buildInitialSelectedKeys(settings: LoadedSettings): string[] {
const preset =
Expand Down Expand Up @@ -151,15 +152,18 @@ export function StatusLineDialog({
[],
);

const terminalHeight = availableTerminalHeight ?? 18;
const hasFullLayout = terminalHeight >= STATUS_LINE_DIALOG_FIXED_ROWS + 1;

const filteredOptions = useMemo(() => {
const normalizedQuery = query.trim().toLowerCase();
if (!normalizedQuery) {
if (!hasFullLayout || !normalizedQuery) {
return options;
}
Comment thread
shenyankm marked this conversation as resolved.
Resolved
return options.filter((option) =>
getOptionSearchText(option).includes(normalizedQuery),
);
}, [options, query]);
}, [hasFullLayout, options, query]);

const presetConfig = useMemo(
() => buildConfigFromKeys(selectedKeys),
Expand Down Expand Up @@ -206,15 +210,18 @@ export function StatusLineDialog({
useKeypress(
(key) => {
if (key.name === 'escape') {
if (query) {
if (hasFullLayout && query) {
setQuery('');
return;
}
onClose();
return;
}

if (key.name === 'backspace' || key.name === 'delete') {
if (
hasFullLayout &&
(key.name === 'backspace' || key.name === 'delete')
) {
Comment thread
shenyankm marked this conversation as resolved.
Resolved
setQuery((current) => current.slice(0, -1));
return;
}
Expand All @@ -230,6 +237,7 @@ export function StatusLineDialog({
}

if (
hasFullLayout &&
!key.ctrl &&
!key.meta &&
key.sequence.length === 1 &&
Expand All @@ -243,30 +251,39 @@ export function StatusLineDialog({
);

const maxItemsToShow = Math.max(
5,
Math.min(10, (availableTerminalHeight ?? 18) - 8),
1,
Math.min(
10,
hasFullLayout
? terminalHeight - STATUS_LINE_DIALOG_FIXED_ROWS
: terminalHeight,
),
);

return (
<Box
borderStyle="round"
borderStyle={hasFullLayout ? 'round' : undefined}
borderColor={theme.border.default}
flexDirection="column"
paddingX={1}
paddingY={1}
paddingY={hasFullLayout ? 1 : 0}
width="100%"
>
<Text bold>Configure Status Line</Text>
<Text color={theme.text.secondary}>
Select which items to display in the status line.
</Text>
{hasFullLayout && (
<>
<Text bold>Configure Status Line</Text>
<Text color={theme.text.secondary}>
Select which items to display in the status line.
</Text>

<Box marginTop={1} flexDirection="column">
<Text color={theme.text.secondary}>Type to search</Text>
<Text>{query ? `> ${query}` : '>'}</Text>
</Box>
<Box marginTop={1} flexDirection="column">
<Text color={theme.text.secondary}>Type to search</Text>
<Text>{query ? `> ${query}` : '>'}</Text>
</Box>
</>
)}

<Box marginTop={1} flexDirection="column">
<Box marginTop={hasFullLayout ? 1 : 0} flexDirection="column">
{filteredOptions.length > 0 ? (
<MultiSelect
items={filteredOptions}
Expand All @@ -283,34 +300,38 @@ export function StatusLineDialog({
)}
</Box>

<Box marginTop={1} flexDirection="column">
<Text color={theme.text.secondary}>Preview</Text>
{previewLines.length > 0 ? (
previewLines.map((line, index) => (
<Text
key={`${line}-${index}`}
color={
presetConfig.useThemeColors ? theme.text.accent : undefined
}
dimColor={!presetConfig.useThemeColors}
wrap="truncate"
>
{line}
</Text>
))
) : (
<Text color={theme.text.secondary}>
Select at least one item to show a status line.
</Text>
)}
</Box>
{hasFullLayout && (
<>
<Box marginTop={1} flexDirection="column">
<Text color={theme.text.secondary}>Preview</Text>
{previewLines.length > 0 ? (
previewLines.map((line, index) => (
<Text
key={`${line}-${index}`}
color={
presetConfig.useThemeColors ? theme.text.accent : undefined
}
dimColor={!presetConfig.useThemeColors}
wrap="truncate"
>
{line}
</Text>
))
) : (
<Text color={theme.text.secondary}>
Select at least one item to show a status line.
</Text>
)}
</Box>

<Box marginTop={1}>
<Text color={theme.text.secondary}>
Use up/down to navigate, space to select, enter to confirm, esc to
cancel
</Text>
</Box>
<Box marginTop={1}>
<Text color={theme.text.secondary}>
Use up/down to navigate, space to select, enter to confirm, esc to
cancel
</Text>
</Box>
</>
)}
</Box>
);
}
Loading
Loading