Fix cluster of bugs in the settings dialog. (#17628)

This commit is contained in:
Jacob Richman
2026-01-27 08:55:35 -08:00
committed by GitHub
parent e5145ab60d
commit 6be42be575
3 changed files with 185 additions and 69 deletions
@@ -307,6 +307,26 @@ describe('SettingsDialog', () => {
// Use snapshot to capture visual layout including indicators // Use snapshot to capture visual layout including indicators
expect(output).toMatchSnapshot(); expect(output).toMatchSnapshot();
}); });
it('should use almost full height of the window but no more when the window height is 25 rows', async () => {
const settings = createMockSettings();
const onSelect = vi.fn();
// Render with a fixed height of 25 rows
const { lastFrame } = renderDialog(settings, onSelect, {
availableTerminalHeight: 25,
});
// Wait for the dialog to render
await waitFor(() => {
const output = lastFrame();
expect(output).toBeDefined();
const lines = output!.split('\n');
expect(lines.length).toBeGreaterThanOrEqual(24);
expect(lines.length).toBeLessThanOrEqual(25);
});
});
}); });
describe('Setting Descriptions', () => { describe('Setting Descriptions', () => {
@@ -1072,6 +1092,87 @@ describe('SettingsDialog', () => {
}); });
}); });
describe('Restart and Search Conflict Regression', () => {
it('should prioritize restart request over search text box when showRestartPrompt is true', async () => {
vi.mocked(getSettingsSchema).mockReturnValue(TOOLS_SHELL_FAKE_SCHEMA);
const settings = createMockSettings();
const onRestartRequest = vi.fn();
const { stdin, lastFrame, unmount } = renderDialog(settings, vi.fn(), {
onRestartRequest,
});
// Wait for initial render
await waitFor(() => expect(lastFrame()).toContain('Show Color'));
// Navigate to "Enable Interactive Shell" (second item in TOOLS_SHELL_FAKE_SCHEMA)
act(() => {
stdin.write(TerminalKeys.DOWN_ARROW);
});
// Wait for navigation to complete
await waitFor(() =>
expect(lastFrame()).toContain('● Enable Interactive Shell'),
);
// Toggle it to trigger restart required
act(() => {
stdin.write(TerminalKeys.ENTER);
});
await waitFor(() => {
expect(lastFrame()).toContain(
'To see changes, Gemini CLI must be restarted',
);
});
// Press 'r' - it should call onRestartRequest, NOT be handled by search
act(() => {
stdin.write('r');
});
await waitFor(() => {
expect(onRestartRequest).toHaveBeenCalled();
});
unmount();
});
it('should hide search box when showRestartPrompt is true', async () => {
vi.mocked(getSettingsSchema).mockReturnValue(TOOLS_SHELL_FAKE_SCHEMA);
const settings = createMockSettings();
const { stdin, lastFrame, unmount } = renderDialog(settings, vi.fn());
// Search box should be visible initially (searchPlaceholder)
expect(lastFrame()).toContain('Search to filter');
// Navigate to "Enable Interactive Shell" and toggle it
act(() => {
stdin.write(TerminalKeys.DOWN_ARROW);
});
await waitFor(() =>
expect(lastFrame()).toContain('● Enable Interactive Shell'),
);
act(() => {
stdin.write(TerminalKeys.ENTER);
});
await waitFor(() => {
expect(lastFrame()).toContain(
'To see changes, Gemini CLI must be restarted',
);
});
// Search box should now be hidden
expect(lastFrame()).not.toContain('Search to filter');
unmount();
});
});
describe('String Settings Editing', () => { describe('String Settings Editing', () => {
it('should allow editing and committing a string setting', async () => { it('should allow editing and committing a string setting', async () => {
let settings = createMockSettings({ 'a.string.setting': 'initial' }); let settings = createMockSettings({ 'a.string.setting': 'initial' });
@@ -601,6 +601,7 @@ export function SettingsDialog({
(key: Key, _currentItem: SettingsDialogItem | undefined): boolean => { (key: Key, _currentItem: SettingsDialogItem | undefined): boolean => {
// 'r' key for restart // 'r' key for restart
if (showRestartPrompt && key.sequence === 'r') { if (showRestartPrompt && key.sequence === 'r') {
saveRestartRequiredSettings();
setShowRestartPrompt(false); setShowRestartPrompt(false);
setModifiedSettings(new Set()); setModifiedSettings(new Set());
setRestartRequiredSettings(new Set()); setRestartRequiredSettings(new Set());
@@ -609,29 +610,37 @@ export function SettingsDialog({
} }
return false; return false;
}, },
[showRestartPrompt, onRestartRequest], [showRestartPrompt, onRestartRequest, saveRestartRequiredSettings],
); );
// Calculate effective max items and scope visibility based on terminal height // Calculate effective max items and scope visibility based on terminal height
const { effectiveMaxItemsToShow, showScopeSelection } = useMemo(() => { const { effectiveMaxItemsToShow, showScopeSelection, showSearch } =
useMemo(() => {
// Only show scope selector if we have a workspace // Only show scope selector if we have a workspace
const hasWorkspace = settings.workspace.path !== undefined; const hasWorkspace = settings.workspace.path !== undefined;
// Search box is hidden when restart prompt is shown to save space and avoid key conflicts
const shouldShowSearch = !showRestartPrompt;
if (!availableTerminalHeight) { if (!availableTerminalHeight) {
return { return {
effectiveMaxItemsToShow: Math.min(MAX_ITEMS_TO_SHOW, items.length), effectiveMaxItemsToShow: Math.min(MAX_ITEMS_TO_SHOW, items.length),
showScopeSelection: hasWorkspace, showScopeSelection: hasWorkspace,
showSearch: shouldShowSearch,
}; };
} }
// Layout constants // Layout constants based on BaseSettingsDialog structure:
const DIALOG_PADDING = 2; // Top and bottom borders // 4 for border (2) and padding (2)
const DIALOG_PADDING = 4;
const SETTINGS_TITLE_HEIGHT = 1; const SETTINGS_TITLE_HEIGHT = 1;
const SEARCH_BOX_HEIGHT = 3; // 3 for box + 1 for marginTop + 1 for spacing after
const SEARCH_SECTION_HEIGHT = shouldShowSearch ? 5 : 0;
const SCROLL_ARROWS_HEIGHT = 2; const SCROLL_ARROWS_HEIGHT = 2;
const SPACING_HEIGHT = 2; const ITEMS_SPACING_AFTER = 1;
const SCOPE_SELECTION_HEIGHT = 4; // 1 for Label + 3 for Scope items + 1 for spacing after
const BOTTOM_HELP_TEXT_HEIGHT = 1; const SCOPE_SECTION_HEIGHT = hasWorkspace ? 5 : 0;
const HELP_TEXT_HEIGHT = 1;
const RESTART_PROMPT_HEIGHT = showRestartPrompt ? 1 : 0; const RESTART_PROMPT_HEIGHT = showRestartPrompt ? 1 : 0;
const ITEM_HEIGHT = 3; // Label + description + spacing const ITEM_HEIGHT = 3; // Label + description + spacing
@@ -639,15 +648,16 @@ export function SettingsDialog({
const baseFixedHeight = const baseFixedHeight =
SETTINGS_TITLE_HEIGHT + SETTINGS_TITLE_HEIGHT +
SEARCH_BOX_HEIGHT + SEARCH_SECTION_HEIGHT +
SCROLL_ARROWS_HEIGHT + SCROLL_ARROWS_HEIGHT +
SPACING_HEIGHT + ITEMS_SPACING_AFTER +
BOTTOM_HELP_TEXT_HEIGHT + HELP_TEXT_HEIGHT +
RESTART_PROMPT_HEIGHT; RESTART_PROMPT_HEIGHT;
// Calculate max items with scope selector // Calculate max items with scope selector
const heightWithScope = baseFixedHeight + SCOPE_SELECTION_HEIGHT; const heightWithScope = baseFixedHeight + SCOPE_SECTION_HEIGHT;
const availableForItemsWithScope = currentAvailableHeight - heightWithScope; const availableForItemsWithScope =
currentAvailableHeight - heightWithScope;
const maxItemsWithScope = Math.max( const maxItemsWithScope = Math.max(
1, 1,
Math.floor(availableForItemsWithScope / ITEM_HEIGHT), Math.floor(availableForItemsWithScope / ITEM_HEIGHT),
@@ -676,6 +686,7 @@ export function SettingsDialog({
return { return {
effectiveMaxItemsToShow: Math.min(maxItems, items.length), effectiveMaxItemsToShow: Math.min(maxItems, items.length),
showScopeSelection: shouldShowScope, showScopeSelection: shouldShowScope,
showSearch: shouldShowSearch,
}; };
}, [ }, [
availableTerminalHeight, availableTerminalHeight,
@@ -695,7 +706,8 @@ export function SettingsDialog({
return ( return (
<BaseSettingsDialog <BaseSettingsDialog
title="Settings" title="Settings"
searchEnabled={true} borderColor={showRestartPrompt ? theme.status.warning : undefined}
searchEnabled={showSearch}
searchBuffer={searchBuffer} searchBuffer={searchBuffer}
items={items} items={items}
showScopeSelector={showScopeSelection} showScopeSelector={showScopeSelection}
@@ -50,6 +50,8 @@ export interface BaseSettingsDialogProps {
// Header // Header
/** Dialog title displayed at the top */ /** Dialog title displayed at the top */
title: string; title: string;
/** Optional border color for the dialog */
borderColor?: string;
// Search (optional feature) // Search (optional feature)
/** Whether to show the search input. Default: true */ /** Whether to show the search input. Default: true */
@@ -107,6 +109,7 @@ export interface BaseSettingsDialogProps {
*/ */
export function BaseSettingsDialog({ export function BaseSettingsDialog({
title, title,
borderColor,
searchEnabled = true, searchEnabled = true,
searchPlaceholder = 'Search to filter', searchPlaceholder = 'Search to filter',
searchBuffer, searchBuffer,
@@ -390,7 +393,7 @@ export function BaseSettingsDialog({
return ( return (
<Box <Box
borderStyle="round" borderStyle="round"
borderColor={theme.border.default} borderColor={borderColor ?? theme.border.default}
flexDirection="row" flexDirection="row"
padding={1} padding={1}
width="100%" width="100%"