Skip to content

Commit d7320f5

Browse files
refactor(core,cli): useAlternateBuffer read from config (google-gemini#20346)
Co-authored-by: Jacob Richman <jacob314@gmail.com>
1 parent 25ade7b commit d7320f5

15 files changed

Lines changed: 164 additions & 41 deletions

packages/cli/src/config/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,7 @@ export async function loadCliConfig(
843843
interactive,
844844
trustedFolder,
845845
useBackgroundColor: settings.ui?.useBackgroundColor,
846+
useAlternateBuffer: settings.ui?.useAlternateBuffer,
846847
useRipgrep: settings.tools?.useRipgrep,
847848
enableInteractiveShell: settings.tools?.shell?.enableInteractiveShell,
848849
shellToolInactivityTimeout: settings.tools?.shell?.inactivityTimeout,

packages/cli/src/gemini.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,7 @@ describe('startInteractiveUI', () => {
11821182
getProjectRoot: () => '/root',
11831183
getScreenReader: () => false,
11841184
getDebugMode: () => false,
1185+
getUseAlternateBuffer: () => true,
11851186
});
11861187
const mockSettings = {
11871188
merged: {

packages/cli/src/gemini.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,8 @@ import { loadSandboxConfig } from './config/sandboxConfig.js';
102102
import { deleteSession, listSessions } from './utils/sessions.js';
103103
import { createPolicyUpdater } from './config/policy.js';
104104
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
105-
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
106105
import { TerminalProvider } from './ui/contexts/TerminalContext.js';
106+
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
107107
import { OverflowProvider } from './ui/contexts/OverflowContext.js';
108108

109109
import { setupTerminalAndTheme } from './utils/terminalTheme.js';
@@ -196,7 +196,7 @@ export async function startInteractiveUI(
196196
// and the Ink alternate buffer mode requires line wrapping harmful to
197197
// screen readers.
198198
const useAlternateBuffer = shouldEnterAlternateScreen(
199-
isAlternateBufferEnabled(settings),
199+
isAlternateBufferEnabled(config),
200200
config.getScreenReader(),
201201
);
202202
const mouseEventsEnabled = useAlternateBuffer;
@@ -678,7 +678,7 @@ export async function main() {
678678

679679
let input = config.getQuestion();
680680
const useAlternateBuffer = shouldEnterAlternateScreen(
681-
isAlternateBufferEnabled(settings),
681+
isAlternateBufferEnabled(config),
682682
config.getScreenReader(),
683683
);
684684
const rawStartupWarnings = await getStartupWarnings();

packages/cli/src/test-utils/mockConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ export const createMockConfig = (overrides: Partial<Config> = {}): Config =>
156156
getExperiments: vi.fn().mockReturnValue(undefined),
157157
getHasAccessToPreviewModel: vi.fn().mockReturnValue(false),
158158
validatePathAccess: vi.fn().mockReturnValue(null),
159+
getUseAlternateBuffer: vi.fn().mockReturnValue(false),
159160
...overrides,
160161
}) as unknown as Config;
161162

packages/cli/src/test-utils/render.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,21 @@ export const renderWithProviders = (
703703
});
704704
}
705705

706+
// Wrap config in a Proxy so useAlternateBuffer hook (which reads from Config) gets the correct value,
707+
// without replacing the entire config object and its other values.
708+
let finalConfig = config;
709+
if (useAlternateBuffer !== undefined) {
710+
finalConfig = new Proxy(config, {
711+
get(target, prop, receiver) {
712+
if (prop === 'getUseAlternateBuffer') {
713+
return () => useAlternateBuffer;
714+
}
715+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
716+
return Reflect.get(target, prop, receiver);
717+
},
718+
});
719+
}
720+
706721
const mainAreaWidth = terminalWidth;
707722

708723
const finalUiState = {
@@ -731,7 +746,7 @@ export const renderWithProviders = (
731746

732747
const renderResult = render(
733748
<AppContext.Provider value={appState}>
734-
<ConfigContext.Provider value={config}>
749+
<ConfigContext.Provider value={finalConfig}>
735750
<SettingsContext.Provider value={finalSettings}>
736751
<UIStateContext.Provider value={finalUiState}>
737752
<VimModeProvider settings={finalSettings}>
@@ -743,7 +758,7 @@ export const renderWithProviders = (
743758
<UIActionsContext.Provider value={finalUIActions}>
744759
<OverflowProvider>
745760
<ToolActionsProvider
746-
config={config}
761+
config={finalConfig}
747762
toolCalls={allToolCalls}
748763
>
749764
<AskUserActionsProvider

packages/cli/src/ui/AppContainer.test.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2675,6 +2675,10 @@ describe('AppContainer State Management', () => {
26752675
isAlternateMode = false,
26762676
childHandler?: Mock,
26772677
) => {
2678+
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(
2679+
isAlternateMode,
2680+
);
2681+
26782682
// Update settings for this test run
26792683
const defaultMergedSettings = mergeSettings({}, {}, {}, {}, true);
26802684
const testSettings = {
@@ -3364,6 +3368,8 @@ describe('AppContainer State Management', () => {
33643368
);
33653369
vi.mocked(checkPermissions).mockResolvedValue([]);
33663370

3371+
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true);
3372+
33673373
let unmount: () => void;
33683374
await act(async () => {
33693375
unmount = renderAppContainer({
@@ -3596,6 +3602,8 @@ describe('AppContainer State Management', () => {
35963602
},
35973603
} as unknown as LoadedSettings;
35983604

3605+
vi.spyOn(mockConfig, 'getUseAlternateBuffer').mockReturnValue(true);
3606+
35993607
let unmount: () => void;
36003608
await act(async () => {
36013609
const result = renderAppContainer({

packages/cli/src/ui/AppContainer.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ import { useSessionResume } from './hooks/useSessionResume.js';
145145
import { useIncludeDirsTrust } from './hooks/useIncludeDirsTrust.js';
146146
import { useSessionRetentionCheck } from './hooks/useSessionRetentionCheck.js';
147147
import { isWorkspaceTrusted } from '../config/trustedFolders.js';
148-
import { useAlternateBuffer } from './hooks/useAlternateBuffer.js';
149148
import { useSettings } from './contexts/SettingsContext.js';
150149
import { terminalCapabilityManager } from './utils/terminalCapabilityManager.js';
151150
import { useInputHistoryStore } from './hooks/useInputHistoryStore.js';
@@ -228,7 +227,7 @@ export const AppContainer = (props: AppContainerProps) => {
228227
});
229228

230229
useMemoryMonitor(historyManager);
231-
const isAlternateBuffer = useAlternateBuffer();
230+
const isAlternateBuffer = config.getUseAlternateBuffer();
232231
const [corgiMode, setCorgiMode] = useState(false);
233232
const [forceRerenderKey, setForceRerenderKey] = useState(0);
234233
const [debugMessage, setDebugMessage] = useState<string>('');
@@ -545,7 +544,7 @@ export const AppContainer = (props: AppContainerProps) => {
545544
const { consoleMessages, clearConsoleMessages: clearConsoleMessagesState } =
546545
useConsoleMessages();
547546

548-
const mainAreaWidth = calculateMainAreaWidth(terminalWidth, settings);
547+
const mainAreaWidth = calculateMainAreaWidth(terminalWidth, config);
549548
// Derive widths for InputPrompt using shared helper
550549
const { inputWidth, suggestionsWidth } = useMemo(() => {
551550
const { inputWidth, suggestionsWidth } =

packages/cli/src/ui/components/ExitPlanModeDialog.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ Implement a comprehensive authentication system with multiple providers.
167167
readTextFile: vi.fn(),
168168
writeTextFile: vi.fn(),
169169
}),
170+
getUseAlternateBuffer: () => options?.useAlternateBuffer ?? true,
170171
} as unknown as import('@google/gemini-cli-core').Config,
171172
},
172173
);
@@ -443,6 +444,7 @@ Implement a comprehensive authentication system with multiple providers.
443444
readTextFile: vi.fn(),
444445
writeTextFile: vi.fn(),
445446
}),
447+
getUseAlternateBuffer: () => useAlternateBuffer ?? true,
446448
} as unknown as import('@google/gemini-cli-core').Config,
447449
},
448450
);

packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ describe('ToolConfirmationQueue', () => {
5151
storage: {
5252
getPlansDir: () => '/mock/temp/plans',
5353
},
54+
getUseAlternateBuffer: () => false,
5455
} as unknown as Config;
5556

5657
beforeEach(() => {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect, vi, beforeEach } from 'vitest';
8+
import { renderHook } from '../../test-utils/render.js';
9+
import {
10+
useAlternateBuffer,
11+
isAlternateBufferEnabled,
12+
} from './useAlternateBuffer.js';
13+
import type { Config } from '@google/gemini-cli-core';
14+
15+
vi.mock('../contexts/ConfigContext.js', () => ({
16+
useConfig: vi.fn(),
17+
}));
18+
19+
const mockUseConfig = vi.mocked(
20+
await import('../contexts/ConfigContext.js').then((m) => m.useConfig),
21+
);
22+
23+
describe('useAlternateBuffer', () => {
24+
beforeEach(() => {
25+
vi.clearAllMocks();
26+
});
27+
28+
it('should return false when config.getUseAlternateBuffer returns false', () => {
29+
mockUseConfig.mockReturnValue({
30+
getUseAlternateBuffer: () => false,
31+
} as unknown as ReturnType<typeof mockUseConfig>);
32+
33+
const { result } = renderHook(() => useAlternateBuffer());
34+
expect(result.current).toBe(false);
35+
});
36+
37+
it('should return true when config.getUseAlternateBuffer returns true', () => {
38+
mockUseConfig.mockReturnValue({
39+
getUseAlternateBuffer: () => true,
40+
} as unknown as ReturnType<typeof mockUseConfig>);
41+
42+
const { result } = renderHook(() => useAlternateBuffer());
43+
expect(result.current).toBe(true);
44+
});
45+
46+
it('should return the immutable config value, not react to settings changes', () => {
47+
const mockConfig = {
48+
getUseAlternateBuffer: () => true,
49+
} as unknown as ReturnType<typeof mockUseConfig>;
50+
51+
mockUseConfig.mockReturnValue(mockConfig);
52+
53+
const { result, rerender } = renderHook(() => useAlternateBuffer());
54+
55+
// Value should remain true even after rerender
56+
expect(result.current).toBe(true);
57+
58+
rerender();
59+
60+
expect(result.current).toBe(true);
61+
});
62+
});
63+
64+
describe('isAlternateBufferEnabled', () => {
65+
it('should return true when config.getUseAlternateBuffer returns true', () => {
66+
const config = {
67+
getUseAlternateBuffer: () => true,
68+
} as unknown as Config;
69+
70+
expect(isAlternateBufferEnabled(config)).toBe(true);
71+
});
72+
73+
it('should return false when config.getUseAlternateBuffer returns false', () => {
74+
const config = {
75+
getUseAlternateBuffer: () => false,
76+
} as unknown as Config;
77+
78+
expect(isAlternateBufferEnabled(config)).toBe(false);
79+
});
80+
});

0 commit comments

Comments
 (0)