diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index 621e16c7df..6548542bda 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -40,6 +40,14 @@ vi.mock('./App.js', () => ({ App: TestContextConsumer, })); +vi.mock('ink', async (importOriginal) => { + const original = await importOriginal(); + return { + ...original, + measureElement: vi.fn(), + }; +}); + vi.mock('./hooks/useQuotaAndFallback.js'); vi.mock('./hooks/useHistoryManager.js'); vi.mock('./hooks/useThemeCommand.js'); @@ -92,6 +100,9 @@ import { useSessionStats } from './contexts/SessionContext.js'; import { useTextBuffer } from './components/shared/text-buffer.js'; import { useLogger } from './hooks/useLogger.js'; import { useLoadingIndicator } from './hooks/useLoadingIndicator.js'; +import { measureElement } from 'ink'; +import { useTerminalSize } from './hooks/useTerminalSize.js'; +import { ShellExecutionService } from '@google/gemini-cli-core'; describe('AppContainer State Management', () => { let mockConfig: Config; @@ -556,4 +567,42 @@ describe('AppContainer State Management', () => { expect(mockHandler).toHaveBeenCalledWith('auth'); }); }); + + describe('Terminal Height Calculation', () => { + const mockedMeasureElement = measureElement as Mock; + const mockedUseTerminalSize = useTerminalSize as Mock; + + it('should prevent terminal height from being less than 1', () => { + const resizePtySpy = vi.spyOn(ShellExecutionService, 'resizePty'); + // Arrange: Simulate a small terminal and a large footer + mockedUseTerminalSize.mockReturnValue({ columns: 80, rows: 5 }); + mockedMeasureElement.mockReturnValue({ width: 80, height: 10 }); // Footer is taller than the screen + mockedUseGeminiStream.mockReturnValue({ + streamingState: 'idle', + submitQuery: vi.fn(), + initError: null, + pendingHistoryItems: [], + thought: null, + cancelOngoingRequest: vi.fn(), + activePtyId: 'some-id', + }); + + render( + , + ); + + // Assert: The shell should be resized to a minimum height of 1, not a negative number. + // The old code would have tried to set a negative height. + expect(resizePtySpy).toHaveBeenCalled(); + const lastCall = + resizePtySpy.mock.calls[resizePtySpy.mock.calls.length - 1]; + // Check the height argument specifically + expect(lastCall[2]).toBe(1); + }); + }); }); diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 37e000490f..1373d29d4f 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -4,7 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { useMemo, useState, useCallback, useEffect, useRef } from 'react'; +import { + useMemo, + useState, + useCallback, + useEffect, + useRef, + useLayoutEffect, +} from 'react'; import { type DOMElement, measureElement } from 'ink'; import { App } from './App.js'; import { AppContext } from './contexts/AppContext.js'; @@ -622,18 +629,27 @@ Logging in with Google... Please restart Gemini CLI to continue. streamingState === StreamingState.Responding) && !proQuotaRequest; - // Compute available terminal height based on controls measurement - const availableTerminalHeight = useMemo(() => { + const [controlsHeight, setControlsHeight] = useState(0); + + useLayoutEffect(() => { if (mainControlsRef.current) { const fullFooterMeasurement = measureElement(mainControlsRef.current); - return terminalHeight - fullFooterMeasurement.height - staticExtraHeight; + if (fullFooterMeasurement.height > 0) { + setControlsHeight(fullFooterMeasurement.height); + } } - return terminalHeight - staticExtraHeight; - }, [terminalHeight]); + }, [buffer, terminalWidth, terminalHeight]); + + // Compute available terminal height based on controls measurement + const availableTerminalHeight = + terminalHeight - controlsHeight - staticExtraHeight; config.setShellExecutionConfig({ terminalWidth: Math.floor(terminalWidth * SHELL_WIDTH_FRACTION), - terminalHeight: Math.floor(availableTerminalHeight - SHELL_HEIGHT_PADDING), + terminalHeight: Math.max( + Math.floor(availableTerminalHeight - SHELL_HEIGHT_PADDING), + 1, + ), pager: settings.merged.tools?.shell?.pager, showColor: settings.merged.tools?.shell?.showColor, }); @@ -660,16 +676,10 @@ Logging in with Google... Please restart Gemini CLI to continue. ShellExecutionService.resizePty( activePtyId, Math.floor(terminalWidth * SHELL_WIDTH_FRACTION), - Math.floor(availableTerminalHeight - SHELL_HEIGHT_PADDING), + Math.max(Math.floor(availableTerminalHeight - SHELL_HEIGHT_PADDING), 1), ); } - }, [ - terminalHeight, - terminalWidth, - availableTerminalHeight, - activePtyId, - geminiClient, - ]); + }, [terminalWidth, availableTerminalHeight, activePtyId]); useEffect(() => { if (