refactor(core,cli): useAlternateBuffer read from config (#20346)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
Pyush Sinha
2026-02-27 07:55:02 -08:00
committed by GitHub
parent 25ade7bcb7
commit d7320f5425
15 changed files with 164 additions and 41 deletions
+1
View File
@@ -843,6 +843,7 @@ export async function loadCliConfig(
interactive, interactive,
trustedFolder, trustedFolder,
useBackgroundColor: settings.ui?.useBackgroundColor, useBackgroundColor: settings.ui?.useBackgroundColor,
useAlternateBuffer: settings.ui?.useAlternateBuffer,
useRipgrep: settings.tools?.useRipgrep, useRipgrep: settings.tools?.useRipgrep,
enableInteractiveShell: settings.tools?.shell?.enableInteractiveShell, enableInteractiveShell: settings.tools?.shell?.enableInteractiveShell,
shellToolInactivityTimeout: settings.tools?.shell?.inactivityTimeout, shellToolInactivityTimeout: settings.tools?.shell?.inactivityTimeout,
+1
View File
@@ -1182,6 +1182,7 @@ describe('startInteractiveUI', () => {
getProjectRoot: () => '/root', getProjectRoot: () => '/root',
getScreenReader: () => false, getScreenReader: () => false,
getDebugMode: () => false, getDebugMode: () => false,
getUseAlternateBuffer: () => true,
}); });
const mockSettings = { const mockSettings = {
merged: { merged: {
+3 -3
View File
@@ -102,8 +102,8 @@ import { loadSandboxConfig } from './config/sandboxConfig.js';
import { deleteSession, listSessions } from './utils/sessions.js'; import { deleteSession, listSessions } from './utils/sessions.js';
import { createPolicyUpdater } from './config/policy.js'; import { createPolicyUpdater } from './config/policy.js';
import { ScrollProvider } from './ui/contexts/ScrollProvider.js'; import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
import { TerminalProvider } from './ui/contexts/TerminalContext.js'; import { TerminalProvider } from './ui/contexts/TerminalContext.js';
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
import { OverflowProvider } from './ui/contexts/OverflowContext.js'; import { OverflowProvider } from './ui/contexts/OverflowContext.js';
import { setupTerminalAndTheme } from './utils/terminalTheme.js'; import { setupTerminalAndTheme } from './utils/terminalTheme.js';
@@ -196,7 +196,7 @@ export async function startInteractiveUI(
// and the Ink alternate buffer mode requires line wrapping harmful to // and the Ink alternate buffer mode requires line wrapping harmful to
// screen readers. // screen readers.
const useAlternateBuffer = shouldEnterAlternateScreen( const useAlternateBuffer = shouldEnterAlternateScreen(
isAlternateBufferEnabled(settings), isAlternateBufferEnabled(config),
config.getScreenReader(), config.getScreenReader(),
); );
const mouseEventsEnabled = useAlternateBuffer; const mouseEventsEnabled = useAlternateBuffer;
@@ -678,7 +678,7 @@ export async function main() {
let input = config.getQuestion(); let input = config.getQuestion();
const useAlternateBuffer = shouldEnterAlternateScreen( const useAlternateBuffer = shouldEnterAlternateScreen(
isAlternateBufferEnabled(settings), isAlternateBufferEnabled(config),
config.getScreenReader(), config.getScreenReader(),
); );
const rawStartupWarnings = await getStartupWarnings(); const rawStartupWarnings = await getStartupWarnings();
@@ -156,6 +156,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
getExperiments: vi.fn().mockReturnValue(undefined), getExperiments: vi.fn().mockReturnValue(undefined),
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false), getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
validatePathAccess: vi.fn().mockReturnValue(null), validatePathAccess: vi.fn().mockReturnValue(null),
getUseAlternateBuffer: vi.fn().mockReturnValue(false),
...overrides, ...overrides,
}) as unknown as Config; }) as unknown as Config;
+17 -2
View File
@@ -703,6 +703,21 @@ export const renderWithProviders = (
}); });
} }
// Wrap config in a Proxy so useAlternateBuffer hook (which reads from Config) gets the correct value,
// without replacing the entire config object and its other values.
let finalConfig = config;
if (useAlternateBuffer !== undefined) {
finalConfig = new Proxy(config, {
get(target, prop, receiver) {
if (prop === 'getUseAlternateBuffer') {
return () => useAlternateBuffer;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Reflect.get(target, prop, receiver);
},
});
}
const mainAreaWidth = terminalWidth; const mainAreaWidth = terminalWidth;
const finalUiState = { const finalUiState = {
@@ -731,7 +746,7 @@ export const renderWithProviders = (
const renderResult = render( const renderResult = render(
<AppContext.Provider value={appState}> <AppContext.Provider value={appState}>
<ConfigContext.Provider value={config}> <ConfigContext.Provider value={finalConfig}>
<SettingsContext.Provider value={finalSettings}> <SettingsContext.Provider value={finalSettings}>
<UIStateContext.Provider value={finalUiState}> <UIStateContext.Provider value={finalUiState}>
<VimModeProvider settings={finalSettings}> <VimModeProvider settings={finalSettings}>
@@ -743,7 +758,7 @@ export const renderWithProviders = (
<UIActionsContext.Provider value={finalUIActions}> <UIActionsContext.Provider value={finalUIActions}>
<OverflowProvider> <OverflowProvider>
<ToolActionsProvider <ToolActionsProvider
config={config} config={finalConfig}
toolCalls={allToolCalls} toolCalls={allToolCalls}
> >
<AskUserActionsProvider <AskUserActionsProvider
@@ -2675,6 +2675,10 @@ describe('AppContainer State Management', () => {
isAlternateMode = false, isAlternateMode = false,
childHandler?: Mock, childHandler?: Mock,
) => { ) => {
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(
isAlternateMode,
);
// Update settings for this test run // Update settings for this test run
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true); const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
const testSettings = { const testSettings = {
@@ -3364,6 +3368,8 @@ describe('AppContainer State Management', () => {
); );
vi.mocked(checkPermissions).mockResolvedValue([]); vi.mocked(checkPermissions).mockResolvedValue([]);
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true);
let unmount: () => void; let unmount: () => void;
await act(async () => { await act(async () => {
unmount = renderAppContainer({ unmount = renderAppContainer({
@@ -3596,6 +3602,8 @@ describe('AppContainer State Management', () => {
}, },
} as unknown as LoadedSettings; } as unknown as LoadedSettings;
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true);
let unmount: () => void; let unmount: () => void;
await act(async () => { await act(async () => {
const result = renderAppContainer({ const result = renderAppContainer({
+2 -3
View File
@@ -145,7 +145,6 @@ import { useSessionResume } from './hooks/useSessionResume.js';
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js'; import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
import { useSessionRetentionCheck } from './hooks/useSessionRetentionCheck.js'; import { useSessionRetentionCheck } from './hooks/useSessionRetentionCheck.js';
import { isWorkspaceTrusted } from '../config/trustedFolders.js'; import { isWorkspaceTrusted } from '../config/trustedFolders.js';
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
import { useSettings } from './contexts/SettingsContext.js'; import { useSettings } from './contexts/SettingsContext.js';
import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js'; import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js';
import { useInputHistoryStore } from './hooks/useInputHistoryStore.js'; import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
@@ -228,7 +227,7 @@ export const AppContainer = (props: AppContainerProps) => {
}); });
useMemoryMonitor(historyManager); useMemoryMonitor(historyManager);
const isAlternateBuffer = useAlternateBuffer(); const isAlternateBuffer = config.getUseAlternateBuffer();
const [corgiMode, setCorgiMode] = useState(false); const [corgiMode, setCorgiMode] = useState(false);
const [forceRerenderKey, setForceRerenderKey] = useState(0); const [forceRerenderKey, setForceRerenderKey] = useState(0);
const [debugMessage, setDebugMessage] = useState<string>(''); const [debugMessage, setDebugMessage] = useState<string>('');
@@ -545,7 +544,7 @@ export const AppContainer = (props: AppContainerProps) => {
const { consoleMessages, clearConsoleMessages: clearConsoleMessagesState } = const { consoleMessages, clearConsoleMessages: clearConsoleMessagesState } =
useConsoleMessages(); useConsoleMessages();
const mainAreaWidth = calculateMainAreaWidth(terminalWidth, settings); const mainAreaWidth = calculateMainAreaWidth(terminalWidth, config);
// Derive widths for InputPrompt using shared helper // Derive widths for InputPrompt using shared helper
const { inputWidth, suggestionsWidth } = useMemo(() => { const { inputWidth, suggestionsWidth } = useMemo(() => {
const { inputWidth, suggestionsWidth } = const { inputWidth, suggestionsWidth } =
@@ -167,6 +167,7 @@ Implement a comprehensive authentication system with multiple providers.
readTextFile: vi.fn(), readTextFile: vi.fn(),
writeTextFile: vi.fn(), writeTextFile: vi.fn(),
}), }),
getUseAlternateBuffer: () => options?.useAlternateBuffer ?? true,
} as unknown as import('@google/gemini-cli-core').Config, } as unknown as import('@google/gemini-cli-core').Config,
}, },
); );
@@ -443,6 +444,7 @@ Implement a comprehensive authentication system with multiple providers.
readTextFile: vi.fn(), readTextFile: vi.fn(),
writeTextFile: vi.fn(), writeTextFile: vi.fn(),
}), }),
getUseAlternateBuffer: () => useAlternateBuffer ?? true,
} as unknown as import('@google/gemini-cli-core').Config, } as unknown as import('@google/gemini-cli-core').Config,
}, },
); );
@@ -51,6 +51,7 @@ describe('ToolConfirmationQueue', () => {
storage: { storage: {
getPlansDir: () => '/mock/temp/plans', getPlansDir: () => '/mock/temp/plans',
}, },
getUseAlternateBuffer: () => false,
} as unknown as Config; } as unknown as Config;
beforeEach(() => { beforeEach(() => {
@@ -0,0 +1,80 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook } from '../../test-utils/render.js';
import {
useAlternateBuffer,
isAlternateBufferEnabled,
} from './useAlternateBuffer.js';
import type { Config } from '@google/gemini-cli-core';
vi.mock('../contexts/ConfigContext.js', () => ({
useConfig: vi.fn(),
}));
const mockUseConfig = vi.mocked(
await import('../contexts/ConfigContext.js').then((m) => m.useConfig),
);
describe('useAlternateBuffer', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should return false when config.getUseAlternateBuffer returns false', () => {
mockUseConfig.mockReturnValue({
getUseAlternateBuffer: () => false,
} as unknown as ReturnType<typeof mockUseConfig>);
const { result } = renderHook(() => useAlternateBuffer());
expect(result.current).toBe(false);
});
it('should return true when config.getUseAlternateBuffer returns true', () => {
mockUseConfig.mockReturnValue({
getUseAlternateBuffer: () => true,
} as unknown as ReturnType<typeof mockUseConfig>);
const { result } = renderHook(() => useAlternateBuffer());
expect(result.current).toBe(true);
});
it('should return the immutable config value, not react to settings changes', () => {
const mockConfig = {
getUseAlternateBuffer: () => true,
} as unknown as ReturnType<typeof mockUseConfig>;
mockUseConfig.mockReturnValue(mockConfig);
const { result, rerender } = renderHook(() => useAlternateBuffer());
// Value should remain true even after rerender
expect(result.current).toBe(true);
rerender();
expect(result.current).toBe(true);
});
});
describe('isAlternateBufferEnabled', () => {
it('should return true when config.getUseAlternateBuffer returns true', () => {
const config = {
getUseAlternateBuffer: () => true,
} as unknown as Config;
expect(isAlternateBufferEnabled(config)).toBe(true);
});
it('should return false when config.getUseAlternateBuffer returns false', () => {
const config = {
getUseAlternateBuffer: () => false,
} as unknown as Config;
expect(isAlternateBufferEnabled(config)).toBe(false);
});
});
@@ -4,13 +4,14 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { useSettings } from '../contexts/SettingsContext.js'; import { useConfig } from '../contexts/ConfigContext.js';
import type { LoadedSettings } from '../../config/settings.js'; import type { Config } from '@google/gemini-cli-core';
export const isAlternateBufferEnabled = (settings: LoadedSettings): boolean => export const isAlternateBufferEnabled = (config: Config): boolean =>
settings.merged.ui.useAlternateBuffer === true; config.getUseAlternateBuffer();
// This is read from Config so that the UI reads the same value per application session
export const useAlternateBuffer = (): boolean => { export const useAlternateBuffer = (): boolean => {
const settings = useSettings(); const config = useConfig();
return isAlternateBufferEnabled(settings); return isAlternateBufferEnabled(config);
}; };
+6 -24
View File
@@ -4,29 +4,11 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect } from 'vitest';
import { calculateMainAreaWidth } from './ui-sizing.js'; import { calculateMainAreaWidth } from './ui-sizing.js';
import { type LoadedSettings } from '../../config/settings.js'; import type { Config } from '@google/gemini-cli-core';
// Mock dependencies
const mocks = vi.hoisted(() => ({
isAlternateBufferEnabled: vi.fn(),
}));
vi.mock('../hooks/useAlternateBuffer.js', () => ({
isAlternateBufferEnabled: mocks.isAlternateBufferEnabled,
}));
describe('ui-sizing', () => { describe('ui-sizing', () => {
const createSettings = (useFullWidth?: boolean): LoadedSettings =>
({
merged: {
ui: {
useFullWidth,
},
},
}) as unknown as LoadedSettings;
describe('calculateMainAreaWidth', () => { describe('calculateMainAreaWidth', () => {
it.each([ it.each([
// expected, width, altBuffer // expected, width, altBuffer
@@ -37,10 +19,10 @@ describe('ui-sizing', () => {
])( ])(
'should return %i when width=%i and altBuffer=%s', 'should return %i when width=%i and altBuffer=%s',
(expected, width, altBuffer) => { (expected, width, altBuffer) => {
mocks.isAlternateBufferEnabled.mockReturnValue(altBuffer); const mockConfig = {
const settings = createSettings(); getUseAlternateBuffer: () => altBuffer,
} as unknown as Config;
expect(calculateMainAreaWidth(width, settings)).toBe(expected); expect(calculateMainAreaWidth(width, mockConfig)).toBe(expected);
}, },
); );
}); });
+3 -3
View File
@@ -4,14 +4,14 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { type LoadedSettings } from '../../config/settings.js'; import type { Config } from '@google/gemini-cli-core';
import { isAlternateBufferEnabled } from '../hooks/useAlternateBuffer.js'; import { isAlternateBufferEnabled } from '../hooks/useAlternateBuffer.js';
export const calculateMainAreaWidth = ( export const calculateMainAreaWidth = (
terminalWidth: number, terminalWidth: number,
settings: LoadedSettings, config: Config,
): number => { ): number => {
if (isAlternateBufferEnabled(settings)) { if (isAlternateBufferEnabled(config)) {
return terminalWidth - 1; return terminalWidth - 1;
} }
return terminalWidth; return terminalWidth;
+25
View File
@@ -941,6 +941,31 @@ describe('Server Config (config.ts)', () => {
}); });
}); });
describe('UseAlternateBuffer Configuration', () => {
it('should default useAlternateBuffer to false when not provided', () => {
const config = new Config(baseParams);
expect(config.getUseAlternateBuffer()).toBe(false);
});
it('should set useAlternateBuffer to true when provided as true', () => {
const paramsWithAlternateBuffer: ConfigParameters = {
...baseParams,
useAlternateBuffer: true,
};
const config = new Config(paramsWithAlternateBuffer);
expect(config.getUseAlternateBuffer()).toBe(true);
});
it('should set useAlternateBuffer to false when explicitly provided as false', () => {
const paramsWithAlternateBuffer: ConfigParameters = {
...baseParams,
useAlternateBuffer: false,
};
const config = new Config(paramsWithAlternateBuffer);
expect(config.getUseAlternateBuffer()).toBe(false);
});
});
describe('UseWriteTodos Configuration', () => { describe('UseWriteTodos Configuration', () => {
it('should default useWriteTodos to true when not provided', () => { it('should default useWriteTodos to true when not provided', () => {
const config = new Config(baseParams); const config = new Config(baseParams);
+7
View File
@@ -519,6 +519,7 @@ export interface ConfigParameters {
interactive?: boolean; interactive?: boolean;
trustedFolder?: boolean; trustedFolder?: boolean;
useBackgroundColor?: boolean; useBackgroundColor?: boolean;
useAlternateBuffer?: boolean;
useRipgrep?: boolean; useRipgrep?: boolean;
enableInteractiveShell?: boolean; enableInteractiveShell?: boolean;
skipNextSpeakerCheck?: boolean; skipNextSpeakerCheck?: boolean;
@@ -702,6 +703,7 @@ export class Config {
private readonly enableInteractiveShell: boolean; private readonly enableInteractiveShell: boolean;
private readonly skipNextSpeakerCheck: boolean; private readonly skipNextSpeakerCheck: boolean;
private readonly useBackgroundColor: boolean; private readonly useBackgroundColor: boolean;
private readonly useAlternateBuffer: boolean;
private shellExecutionConfig: ShellExecutionConfig; private shellExecutionConfig: ShellExecutionConfig;
private readonly extensionManagement: boolean = true; private readonly extensionManagement: boolean = true;
private readonly truncateToolOutputThreshold: number; private readonly truncateToolOutputThreshold: number;
@@ -900,6 +902,7 @@ export class Config {
this.directWebFetch = params.directWebFetch ?? false; this.directWebFetch = params.directWebFetch ?? false;
this.useRipgrep = params.useRipgrep ?? true; this.useRipgrep = params.useRipgrep ?? true;
this.useBackgroundColor = params.useBackgroundColor ?? true; this.useBackgroundColor = params.useBackgroundColor ?? true;
this.useAlternateBuffer = params.useAlternateBuffer ?? false;
this.enableInteractiveShell = params.enableInteractiveShell ?? false; this.enableInteractiveShell = params.enableInteractiveShell ?? false;
this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? true; this.skipNextSpeakerCheck = params.skipNextSpeakerCheck ?? true;
this.shellExecutionConfig = { this.shellExecutionConfig = {
@@ -2521,6 +2524,10 @@ export class Config {
return this.useBackgroundColor; return this.useBackgroundColor;
} }
getUseAlternateBuffer(): boolean {
return this.useAlternateBuffer;
}
getEnableInteractiveShell(): boolean { getEnableInteractiveShell(): boolean {
return this.enableInteractiveShell; return this.enableInteractiveShell;
} }