diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index 412ba01b310..aa830c02502 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -102,6 +102,7 @@ import { createPolicyUpdater } from './config/policy.js';
import { ScrollProvider } from './ui/contexts/ScrollProvider.js';
import { isAlternateBufferEnabled } from './ui/hooks/useAlternateBuffer.js';
import { TerminalProvider } from './ui/contexts/TerminalContext.js';
+import { OverflowProvider } from './ui/contexts/OverflowContext.js';
import { setupTerminalAndTheme } from './utils/terminalTheme.js';
import { profiler } from './ui/components/DebugProfiler.js';
@@ -238,17 +239,19 @@ export async function startInteractiveUI(
>
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx
index 257ea844661..a13d6e25583 100644
--- a/packages/cli/src/test-utils/render.tsx
+++ b/packages/cli/src/test-utils/render.tsx
@@ -35,6 +35,13 @@ import { type HistoryItemToolGroup, StreamingState } from '../ui/types.js';
import { ToolActionsProvider } from '../ui/contexts/ToolActionsContext.js';
import { AskUserActionsProvider } from '../ui/contexts/AskUserActionsContext.js';
import { TerminalProvider } from '../ui/contexts/TerminalContext.js';
+import {
+ OverflowProvider,
+ useOverflowActions,
+ useOverflowState,
+ type OverflowActions,
+ type OverflowState,
+} from '../ui/contexts/OverflowContext.js';
import { makeFakeConfig, type Config } from '@google/gemini-cli-core';
import { FakePersistentState } from './persistentStateFake.js';
@@ -335,6 +342,8 @@ export type RenderInstance = {
lastFrame: (options?: { allowEmpty?: boolean }) => string;
terminal: Terminal;
waitUntilReady: () => Promise;
+ capturedOverflowState: OverflowState | undefined;
+ capturedOverflowActions: OverflowActions | undefined;
};
const instances: InkInstance[] = [];
@@ -343,7 +352,10 @@ const instances: InkInstance[] = [];
export const render = (
tree: React.ReactElement,
terminalWidth?: number,
-): RenderInstance => {
+): Omit<
+ RenderInstance,
+ 'capturedOverflowState' | 'capturedOverflowActions'
+> => {
const cols = terminalWidth ?? 100;
// We use 1000 rows to avoid windows with incorrect snapshots if a correct
// value was used (e.g. 40 rows). The alternatives to make things worse are
@@ -562,6 +574,16 @@ const mockUIActions: UIActions = {
handleNewAgentsSelect: vi.fn(),
};
+let capturedOverflowState: OverflowState | undefined;
+let capturedOverflowActions: OverflowActions | undefined;
+const ContextCapture: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ capturedOverflowState = useOverflowState();
+ capturedOverflowActions = useOverflowActions();
+ return <>{children}>;
+};
+
export const renderWithProviders = (
component: React.ReactElement,
{
@@ -663,6 +685,9 @@ export const renderWithProviders = (
.filter((item): item is HistoryItemToolGroup => item.type === 'tool_group')
.flatMap((item) => item.tools);
+ capturedOverflowState = undefined;
+ capturedOverflowActions = undefined;
+
const renderResult = render(
@@ -675,35 +700,39 @@ export const renderWithProviders = (
value={finalUiState.streamingState}
>
-
-
+
-
-
-
-
-
- {component}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ {component}
+
+
+
+
+
+
+
+
+
@@ -718,6 +747,8 @@ export const renderWithProviders = (
return {
...renderResult,
+ capturedOverflowState,
+ capturedOverflowActions,
simulateClick: (col: number, row: number, button?: 0 | 1 | 2) =>
simulateClick(renderResult.stdin, col, row, button),
};
diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx
index 3aeef34292f..b3610a6d728 100644
--- a/packages/cli/src/ui/AppContainer.test.tsx
+++ b/packages/cli/src/ui/AppContainer.test.tsx
@@ -105,6 +105,11 @@ import {
type UIActions,
} from './contexts/UIActionsContext.js';
import { KeypressProvider } from './contexts/KeypressContext.js';
+import { OverflowProvider } from './contexts/OverflowContext.js';
+import {
+ useOverflowActions,
+ type OverflowActions,
+} from './contexts/OverflowContext.js';
// Mock useStdout to capture terminal title writes
vi.mock('ink', async (importOriginal) => {
@@ -120,9 +125,11 @@ vi.mock('ink', async (importOriginal) => {
// so we can assert against them in our tests.
let capturedUIState: UIState;
let capturedUIActions: UIActions;
+let capturedOverflowActions: OverflowActions;
function TestContextConsumer() {
capturedUIState = useContext(UIStateContext)!;
capturedUIActions = useContext(UIActionsContext)!;
+ capturedOverflowActions = useOverflowActions()!;
return null;
}
@@ -229,7 +236,10 @@ import {
disableMouseEvents,
} from '@google/gemini-cli-core';
import { type ExtensionManager } from '../config/extension-manager.js';
-import { WARNING_PROMPT_DURATION_MS } from './constants.js';
+import {
+ WARNING_PROMPT_DURATION_MS,
+ EXPAND_HINT_DURATION_MS,
+} from './constants.js';
describe('AppContainer State Management', () => {
let mockConfig: Config;
@@ -255,13 +265,15 @@ describe('AppContainer State Management', () => {
} = {}) => (
-
+
+
+
);
@@ -2687,12 +2699,14 @@ describe('AppContainer State Management', () => {
const getTree = (settings: LoadedSettings) => (
-
-
+
+
+
+
);
@@ -3303,6 +3317,306 @@ describe('AppContainer State Management', () => {
});
});
+ describe('Submission Handling', () => {
+ it('resets expansion state on submission when not in alternate buffer', async () => {
+ const { checkPermissions } = await import(
+ './hooks/atCommandProcessor.js'
+ );
+ vi.mocked(checkPermissions).mockResolvedValue([]);
+
+ let unmount: () => void;
+ await act(async () => {
+ unmount = renderAppContainer({
+ settings: {
+ ...mockSettings,
+ merged: {
+ ...mockSettings.merged,
+ ui: { ...mockSettings.merged.ui, useAlternateBuffer: false },
+ },
+ } as LoadedSettings,
+ }).unmount;
+ });
+
+ await waitFor(() => expect(capturedUIActions).toBeTruthy());
+
+ // Expand first
+ act(() => capturedUIActions.setConstrainHeight(false));
+ expect(capturedUIState.constrainHeight).toBe(false);
+
+ // Reset mock stdout to clear any initial writes
+ mocks.mockStdout.write.mockClear();
+
+ // Submit
+ await act(async () => capturedUIActions.handleFinalSubmit('test prompt'));
+
+ // Should be reset
+ expect(capturedUIState.constrainHeight).toBe(true);
+ // Should refresh static (which clears terminal in non-alternate buffer)
+ expect(mocks.mockStdout.write).toHaveBeenCalledWith(
+ ansiEscapes.clearTerminal,
+ );
+ unmount!();
+ });
+
+ it('resets expansion state on submission when in alternate buffer without clearing terminal', async () => {
+ const { checkPermissions } = await import(
+ './hooks/atCommandProcessor.js'
+ );
+ vi.mocked(checkPermissions).mockResolvedValue([]);
+
+ let unmount: () => void;
+ await act(async () => {
+ unmount = renderAppContainer({
+ settings: {
+ ...mockSettings,
+ merged: {
+ ...mockSettings.merged,
+ ui: { ...mockSettings.merged.ui, useAlternateBuffer: true },
+ },
+ } as LoadedSettings,
+ }).unmount;
+ });
+
+ await waitFor(() => expect(capturedUIActions).toBeTruthy());
+
+ // Expand first
+ act(() => capturedUIActions.setConstrainHeight(false));
+ expect(capturedUIState.constrainHeight).toBe(false);
+
+ // Reset mock stdout
+ mocks.mockStdout.write.mockClear();
+
+ // Submit
+ await act(async () => capturedUIActions.handleFinalSubmit('test prompt'));
+
+ // Should be reset
+ expect(capturedUIState.constrainHeight).toBe(true);
+ // Should NOT refresh static's clearTerminal in alternate buffer
+ expect(mocks.mockStdout.write).not.toHaveBeenCalledWith(
+ ansiEscapes.clearTerminal,
+ );
+ unmount!();
+ });
+ });
+
+ describe('Overflow Hint Handling', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('sets showIsExpandableHint when overflow occurs in Standard Mode and hides after 10s', async () => {
+ let unmount: () => void;
+ await act(async () => {
+ const result = renderAppContainer();
+ unmount = result.unmount;
+ });
+ await waitFor(() => expect(capturedUIState).toBeTruthy());
+
+ // Trigger overflow
+ act(() => {
+ capturedOverflowActions.addOverflowingId('test-id');
+ });
+
+ await waitFor(() => {
+ // Should show hint because we are in Standard Mode (default settings) and have overflow
+ expect(capturedUIState.showIsExpandableHint).toBe(true);
+ });
+
+ // Advance just before the timeout
+ act(() => {
+ vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS - 100);
+ });
+ expect(capturedUIState.showIsExpandableHint).toBe(true);
+
+ // Advance to hit the timeout mark
+ act(() => {
+ vi.advanceTimersByTime(100);
+ });
+ await waitFor(() => {
+ expect(capturedUIState.showIsExpandableHint).toBe(false);
+ });
+
+ unmount!();
+ });
+
+ it('toggles expansion state and resets the hint timer when Ctrl+O is pressed in Standard Mode', async () => {
+ let unmount: () => void;
+ let stdin: ReturnType['stdin'];
+ await act(async () => {
+ const result = renderAppContainer();
+ unmount = result.unmount;
+ stdin = result.stdin;
+ });
+ await waitFor(() => expect(capturedUIState).toBeTruthy());
+
+ // Initial state is constrainHeight = true
+ expect(capturedUIState.constrainHeight).toBe(true);
+
+ // Trigger overflow so the hint starts showing
+ act(() => {
+ capturedOverflowActions.addOverflowingId('test-id');
+ });
+
+ await waitFor(() => {
+ expect(capturedUIState.showIsExpandableHint).toBe(true);
+ });
+
+ // Advance half the duration
+ act(() => {
+ vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
+ });
+ expect(capturedUIState.showIsExpandableHint).toBe(true);
+
+ // Simulate Ctrl+O
+ act(() => {
+ stdin.write('\x0f'); // \x0f is Ctrl+O
+ });
+
+ await waitFor(() => {
+ // constrainHeight should toggle
+ expect(capturedUIState.constrainHeight).toBe(false);
+ });
+
+ // Advance enough that the original timer would have expired if it hadn't reset
+ act(() => {
+ vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2 + 1000);
+ });
+
+ // We expect it to still be true because Ctrl+O should have reset the timer
+ expect(capturedUIState.showIsExpandableHint).toBe(true);
+
+ // Advance remaining time to reach the new timeout
+ act(() => {
+ vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2 - 1000);
+ });
+
+ await waitFor(() => {
+ expect(capturedUIState.showIsExpandableHint).toBe(false);
+ });
+
+ unmount!();
+ });
+
+ it('toggles Ctrl+O multiple times and verifies the hint disappears exactly after the last toggle', async () => {
+ let unmount: () => void;
+ let stdin: ReturnType['stdin'];
+ await act(async () => {
+ const result = renderAppContainer();
+ unmount = result.unmount;
+ stdin = result.stdin;
+ });
+ await waitFor(() => expect(capturedUIState).toBeTruthy());
+
+ // Initial state is constrainHeight = true
+ expect(capturedUIState.constrainHeight).toBe(true);
+
+ // Trigger overflow so the hint starts showing
+ act(() => {
+ capturedOverflowActions.addOverflowingId('test-id');
+ });
+
+ await waitFor(() => {
+ expect(capturedUIState.showIsExpandableHint).toBe(true);
+ });
+
+ // Advance half the duration
+ act(() => {
+ vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
+ });
+ expect(capturedUIState.showIsExpandableHint).toBe(true);
+
+ // First toggle 'on' (expanded)
+ act(() => {
+ stdin.write('\x0f'); // Ctrl+O
+ });
+ await waitFor(() => {
+ expect(capturedUIState.constrainHeight).toBe(false);
+ });
+
+ // Wait 1 second
+ act(() => {
+ vi.advanceTimersByTime(1000);
+ });
+ expect(capturedUIState.showIsExpandableHint).toBe(true);
+
+ // Second toggle 'off' (collapsed)
+ act(() => {
+ stdin.write('\x0f'); // Ctrl+O
+ });
+ await waitFor(() => {
+ expect(capturedUIState.constrainHeight).toBe(true);
+ });
+
+ // Wait 1 second
+ act(() => {
+ vi.advanceTimersByTime(1000);
+ });
+ expect(capturedUIState.showIsExpandableHint).toBe(true);
+
+ // Third toggle 'on' (expanded)
+ act(() => {
+ stdin.write('\x0f'); // Ctrl+O
+ });
+ await waitFor(() => {
+ expect(capturedUIState.constrainHeight).toBe(false);
+ });
+
+ // Now we wait just before the timeout from the LAST toggle.
+ // It should still be true.
+ act(() => {
+ vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS - 100);
+ });
+ expect(capturedUIState.showIsExpandableHint).toBe(true);
+
+ // Wait 0.1s more to hit exactly the timeout since the last toggle.
+ // It should hide now.
+ act(() => {
+ vi.advanceTimersByTime(100);
+ });
+ await waitFor(() => {
+ expect(capturedUIState.showIsExpandableHint).toBe(false);
+ });
+
+ unmount!();
+ });
+
+ it('does NOT set showIsExpandableHint when overflow occurs in Alternate Buffer Mode', async () => {
+ const alternateSettings = mergeSettings({}, {}, {}, {}, true);
+ const settingsWithAlternateBuffer = {
+ merged: {
+ ...alternateSettings,
+ ui: {
+ ...alternateSettings.ui,
+ useAlternateBuffer: true,
+ },
+ },
+ } as unknown as LoadedSettings;
+
+ let unmount: () => void;
+ await act(async () => {
+ const result = renderAppContainer({
+ settings: settingsWithAlternateBuffer,
+ });
+ unmount = result.unmount;
+ });
+ await waitFor(() => expect(capturedUIState).toBeTruthy());
+
+ // Trigger overflow
+ act(() => {
+ capturedOverflowActions.addOverflowingId('test-id');
+ });
+
+ // Should NOT show hint because we are in Alternate Buffer Mode
+ expect(capturedUIState.showIsExpandableHint).toBe(false);
+
+ unmount!();
+ });
+ });
+
describe('Permission Handling', () => {
it('shows permission dialog when checkPermissions returns paths', async () => {
const { checkPermissions } = await import(
diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx
index bdf9501c4ea..d0ba22d4559 100644
--- a/packages/cli/src/ui/AppContainer.tsx
+++ b/packages/cli/src/ui/AppContainer.tsx
@@ -95,6 +95,10 @@ import { useSettingsCommand } from './hooks/useSettingsCommand.js';
import { useModelCommand } from './hooks/useModelCommand.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useVimMode } from './contexts/VimModeContext.js';
+import {
+ useOverflowActions,
+ useOverflowState,
+} from './contexts/OverflowContext.js';
import { useConsoleMessages } from './hooks/useConsoleMessages.js';
import { useTerminalSize } from './hooks/useTerminalSize.js';
import { calculatePromptWidths } from './components/InputPrompt.js';
@@ -151,6 +155,7 @@ import { useBackgroundShellManager } from './hooks/useBackgroundShellManager.js'
import {
WARNING_PROMPT_DURATION_MS,
QUEUE_ERROR_DISPLAY_DURATION_MS,
+ EXPAND_HINT_DURATION_MS,
} from './constants.js';
import { LoginWithGoogleRestartDialog } from './auth/LoginWithGoogleRestartDialog.js';
import { NewAgentsChoice } from './components/NewAgentsNotification.js';
@@ -214,6 +219,7 @@ const SHELL_HEIGHT_PADDING = 10;
export const AppContainer = (props: AppContainerProps) => {
const { config, initializationResult, resumedSessionData } = props;
const settings = useSettings();
+ const { reset } = useOverflowActions()!;
const notificationsEnabled = isNotificationsEnabled(settings);
const historyManager = useHistory({
@@ -262,6 +268,54 @@ export const AppContainer = (props: AppContainerProps) => {
);
const [newAgents, setNewAgents] = useState(null);
+ const [constrainHeight, setConstrainHeight] = useState(true);
+ const [showIsExpandableHint, setShowIsExpandableHint] = useState(false);
+ const expandHintTimerRef = useRef(null);
+ const overflowState = useOverflowState();
+ const overflowingIdsSize = overflowState?.overflowingIds.size ?? 0;
+ const hasOverflowState = overflowingIdsSize > 0 || !constrainHeight;
+
+ /**
+ * Manages the visibility and x-second timer for the expansion hint.
+ *
+ * This effect triggers the timer countdown whenever an overflow is detected
+ * or the user manually toggles the expansion state with Ctrl+O. We use a stable
+ * boolean dependency (hasOverflowState) to ensure the timer only resets on
+ * genuine state transitions, preventing it from infinitely resetting during
+ * active text streaming.
+ */
+ useEffect(() => {
+ if (isAlternateBuffer) {
+ setShowIsExpandableHint(false);
+ if (expandHintTimerRef.current) {
+ clearTimeout(expandHintTimerRef.current);
+ }
+ return;
+ }
+
+ if (hasOverflowState) {
+ setShowIsExpandableHint(true);
+ if (expandHintTimerRef.current) {
+ clearTimeout(expandHintTimerRef.current);
+ }
+ expandHintTimerRef.current = setTimeout(() => {
+ setShowIsExpandableHint(false);
+ }, EXPAND_HINT_DURATION_MS);
+ }
+ }, [hasOverflowState, isAlternateBuffer, constrainHeight]);
+
+ /**
+ * Safe cleanup to ensure the expansion hint timer is cancelled when the
+ * component unmounts, preventing memory leaks.
+ */
+ useEffect(
+ () => () => {
+ if (expandHintTimerRef.current) {
+ clearTimeout(expandHintTimerRef.current);
+ }
+ },
+ [],
+ );
const [defaultBannerText, setDefaultBannerText] = useState('');
const [warningBannerText, setWarningBannerText] = useState('');
@@ -1189,6 +1243,19 @@ Logging in with Google... Restarting Gemini CLI to continue.
const handleFinalSubmit = useCallback(
async (submittedValue: string) => {
+ reset();
+ // Explicitly hide the expansion hint and clear its x-second timer when a new turn begins.
+ setShowIsExpandableHint(false);
+ if (expandHintTimerRef.current) {
+ clearTimeout(expandHintTimerRef.current);
+ }
+ if (!constrainHeight) {
+ setConstrainHeight(true);
+ if (!isAlternateBuffer) {
+ refreshStatic();
+ }
+ }
+
const isSlash = isSlashCommand(submittedValue.trim());
const isIdle = streamingState === StreamingState.Idle;
const isAgentRunning =
@@ -1247,15 +1314,32 @@ Logging in with Google... Restarting Gemini CLI to continue.
pendingSlashCommandHistoryItems,
pendingGeminiHistoryItems,
config,
+ constrainHeight,
+ setConstrainHeight,
+ isAlternateBuffer,
+ refreshStatic,
+ reset,
handleHintSubmit,
],
);
const handleClearScreen = useCallback(() => {
+ reset();
+ // Explicitly hide the expansion hint and clear its x-second timer when clearing the screen.
+ setShowIsExpandableHint(false);
+ if (expandHintTimerRef.current) {
+ clearTimeout(expandHintTimerRef.current);
+ }
historyManager.clearItems();
clearConsoleMessagesState();
refreshStatic();
- }, [historyManager, clearConsoleMessagesState, refreshStatic]);
+ }, [
+ historyManager,
+ clearConsoleMessagesState,
+ refreshStatic,
+ reset,
+ setShowIsExpandableHint,
+ ]);
const { handleInput: vimHandleInput } = useVim(buffer, handleFinalSubmit);
@@ -1425,7 +1509,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
windowMs: WARNING_PROMPT_DURATION_MS,
onRepeat: handleExitRepeat,
});
- const [constrainHeight, setConstrainHeight] = useState(true);
+
const [ideContextState, setIdeContextState] = useState<
IdeContext | undefined
>();
@@ -1655,6 +1739,19 @@ Logging in with Google... Restarting Gemini CLI to continue.
if (!constrainHeight) {
enteringConstrainHeightMode = true;
setConstrainHeight(true);
+ if (keyMatchers[Command.SHOW_MORE_LINES](key)) {
+ // If the user manually collapses the view, show the hint and reset the x-second timer.
+ setShowIsExpandableHint(true);
+ if (expandHintTimerRef.current) {
+ clearTimeout(expandHintTimerRef.current);
+ }
+ expandHintTimerRef.current = setTimeout(() => {
+ setShowIsExpandableHint(false);
+ }, EXPAND_HINT_DURATION_MS);
+ }
+ if (!isAlternateBuffer) {
+ refreshStatic();
+ }
}
if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
@@ -1698,6 +1795,17 @@ Logging in with Google... Restarting Gemini CLI to continue.
!enteringConstrainHeightMode
) {
setConstrainHeight(false);
+ // If the user manually expands the view, show the hint and reset the x-second timer.
+ setShowIsExpandableHint(true);
+ if (expandHintTimerRef.current) {
+ clearTimeout(expandHintTimerRef.current);
+ }
+ expandHintTimerRef.current = setTimeout(() => {
+ setShowIsExpandableHint(false);
+ }, EXPAND_HINT_DURATION_MS);
+ if (!isAlternateBuffer) {
+ refreshStatic();
+ }
return true;
} else if (
(keyMatchers[Command.FOCUS_SHELL_INPUT](key) ||
@@ -2218,6 +2326,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
isBackgroundShellListOpen,
adminSettingsChanged,
newAgents,
+ showIsExpandableHint,
hintMode:
config.isModelSteeringEnabled() &&
isToolExecuting([
@@ -2344,6 +2453,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
backgroundShells,
adminSettingsChanged,
newAgents,
+ showIsExpandableHint,
],
);
diff --git a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx
index a227047fba6..07693db1513 100644
--- a/packages/cli/src/ui/components/FolderTrustDialog.test.tsx
+++ b/packages/cli/src/ui/components/FolderTrustDialog.test.tsx
@@ -173,7 +173,9 @@ describe('FolderTrustDialog', () => {
// Initial state: truncated
await waitFor(() => {
expect(lastFrame()).toContain('Do you trust the files in this folder?');
- expect(lastFrame()).toContain('Press ctrl-o to show more lines');
+ // In standard terminal mode, the expansion hint is handled globally by ToastDisplay
+ // via AppContainer, so it should not be present in the dialog's local frame.
+ expect(lastFrame()).not.toContain('Press Ctrl+O');
expect(lastFrame()).toContain('hidden');
});
diff --git a/packages/cli/src/ui/components/FolderTrustDialog.tsx b/packages/cli/src/ui/components/FolderTrustDialog.tsx
index 70cfd9fd4c8..2067a5dc3a4 100644
--- a/packages/cli/src/ui/components/FolderTrustDialog.tsx
+++ b/packages/cli/src/ui/components/FolderTrustDialog.tsx
@@ -285,33 +285,37 @@ export const FolderTrustDialog: React.FC = ({
);
};
- return (
-
-
-
- {renderContent()}
-
+ const content = (
+
+
+ {renderContent()}
+
+
+
+
+
-
-
+ {isRestarting && (
+
+
+ Gemini CLI is restarting to apply the trust changes...
+
+
+ )}
+ {exiting && (
+
+
+ A folder trust level must be selected to continue. Exiting since
+ escape was pressed.
+
+ )}
+
+ );
- {isRestarting && (
-
-
- Gemini CLI is restarting to apply the trust changes...
-
-
- )}
- {exiting && (
-
-
- A folder trust level must be selected to continue. Exiting since
- escape was pressed.
-
-
- )}
-
-
+ return isAlternateBuffer ? (
+ {content}
+ ) : (
+ content
);
};
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
index f863e2272af..2fceacdf2f4 100644
--- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
@@ -46,6 +46,7 @@ interface HistoryItemDisplayProps {
isPending: boolean;
commands?: readonly SlashCommand[];
availableTerminalHeightGemini?: number;
+ isExpandable?: boolean;
}
export const HistoryItemDisplay: React.FC = ({
@@ -55,6 +56,7 @@ export const HistoryItemDisplay: React.FC = ({
isPending,
commands,
availableTerminalHeightGemini,
+ isExpandable,
}) => {
const settings = useSettings();
const inlineThinkingMode = getInlineThinkingMode(settings);
@@ -180,6 +182,7 @@ export const HistoryItemDisplay: React.FC = ({
terminalWidth={terminalWidth}
borderTop={itemForDisplay.borderTop}
borderBottom={itemForDisplay.borderBottom}
+ isExpandable={isExpandable}
/>
)}
{itemForDisplay.type === 'compression' && (
diff --git a/packages/cli/src/ui/components/MainContent.test.tsx b/packages/cli/src/ui/components/MainContent.test.tsx
index fce375c3065..598d19240fa 100644
--- a/packages/cli/src/ui/components/MainContent.test.tsx
+++ b/packages/cli/src/ui/components/MainContent.test.tsx
@@ -493,7 +493,8 @@ describe('MainContent', () => {
isAlternateBuffer: true,
embeddedShellFocused: true,
constrainHeight: true,
- shouldShowLine1: true,
+ shouldShowLine1: false,
+ staticAreaMaxItemHeight: 15,
},
{
name: 'ASB mode - Unfocused shell',
@@ -501,6 +502,7 @@ describe('MainContent', () => {
embeddedShellFocused: false,
constrainHeight: true,
shouldShowLine1: false,
+ staticAreaMaxItemHeight: 15,
},
{
name: 'Normal mode - Constrained height',
@@ -508,13 +510,15 @@ describe('MainContent', () => {
embeddedShellFocused: false,
constrainHeight: true,
shouldShowLine1: false,
+ staticAreaMaxItemHeight: 15,
},
{
name: 'Normal mode - Unconstrained height',
isAlternateBuffer: false,
embeddedShellFocused: false,
constrainHeight: false,
- shouldShowLine1: false,
+ shouldShowLine1: true,
+ staticAreaMaxItemHeight: 15,
},
];
@@ -525,6 +529,7 @@ describe('MainContent', () => {
embeddedShellFocused,
constrainHeight,
shouldShowLine1,
+ staticAreaMaxItemHeight,
}) => {
vi.mocked(useAlternateBuffer).mockReturnValue(isAlternateBuffer);
const ptyId = 123;
@@ -554,6 +559,7 @@ describe('MainContent', () => {
},
],
availableTerminalHeight: 30, // In ASB mode, focused shell should get ~28 lines
+ staticAreaMaxItemHeight,
terminalHeight: 50,
terminalWidth: 100,
mainAreaWidth: 100,
diff --git a/packages/cli/src/ui/components/MainContent.tsx b/packages/cli/src/ui/components/MainContent.tsx
index cba57756e32..fbcc9626637 100644
--- a/packages/cli/src/ui/components/MainContent.tsx
+++ b/packages/cli/src/ui/components/MainContent.tsx
@@ -47,32 +47,61 @@ export const MainContent = () => {
pendingHistoryItems,
mainAreaWidth,
staticAreaMaxItemHeight,
- availableTerminalHeight,
cleanUiDetailsVisible,
} = uiState;
const showHeaderDetails = cleanUiDetailsVisible;
+ const lastUserPromptIndex = useMemo(() => {
+ for (let i = uiState.history.length - 1; i >= 0; i--) {
+ const type = uiState.history[i].type;
+ if (type === 'user' || type === 'user_shell') {
+ return i;
+ }
+ }
+ return -1;
+ }, [uiState.history]);
+
const historyItems = useMemo(
() =>
- uiState.history.map((h) => (
-
- )),
+ uiState.history.map((h, index) => {
+ const isExpandable = index > lastUserPromptIndex;
+ return (
+
+ );
+ }),
[
uiState.history,
mainAreaWidth,
staticAreaMaxItemHeight,
uiState.slashCommands,
+ uiState.constrainHeight,
+ lastUserPromptIndex,
],
);
+ const staticHistoryItems = useMemo(
+ () => historyItems.slice(0, lastUserPromptIndex + 1),
+ [historyItems, lastUserPromptIndex],
+ );
+
+ const lastResponseHistoryItems = useMemo(
+ () => historyItems.slice(lastUserPromptIndex + 1),
+ [historyItems, lastUserPromptIndex],
+ );
+
const pendingItems = useMemo(
() => (
@@ -80,14 +109,12 @@ export const MainContent = () => {
))}
{showConfirmationQueue && confirmingTool && (
@@ -98,8 +125,7 @@ export const MainContent = () => {
[
pendingHistoryItems,
uiState.constrainHeight,
- isAlternateBuffer,
- availableTerminalHeight,
+ staticAreaMaxItemHeight,
mainAreaWidth,
showConfirmationQueue,
confirmingTool,
@@ -109,10 +135,14 @@ export const MainContent = () => {
const virtualizedData = useMemo(
() => [
{ type: 'header' as const },
- ...uiState.history.map((item) => ({ type: 'history' as const, item })),
+ ...uiState.history.map((item, index) => ({
+ type: 'history' as const,
+ item,
+ isExpandable: index > lastUserPromptIndex,
+ })),
{ type: 'pending' as const },
],
- [uiState.history],
+ [uiState.history, lastUserPromptIndex],
);
const renderItem = useCallback(
@@ -129,12 +159,17 @@ export const MainContent = () => {
return (
);
} else {
@@ -147,6 +182,8 @@ export const MainContent = () => {
mainAreaWidth,
uiState.slashCommands,
pendingItems,
+ uiState.constrainHeight,
+ staticAreaMaxItemHeight,
],
);
@@ -176,7 +213,8 @@ export const MainContent = () => {
key={uiState.historyRemountKey}
items={[
,
- ...historyItems,
+ ...staticHistoryItems,
+ ...lastResponseHistoryItems,
]}
>
{(item) => item}
diff --git a/packages/cli/src/ui/components/ShowMoreLines.test.tsx b/packages/cli/src/ui/components/ShowMoreLines.test.tsx
index 699e2b7f019..4a6829809ae 100644
--- a/packages/cli/src/ui/components/ShowMoreLines.test.tsx
+++ b/packages/cli/src/ui/components/ShowMoreLines.test.tsx
@@ -29,7 +29,6 @@ describe('ShowMoreLines', () => {
it.each([
[new Set(), StreamingState.Idle, true], // No overflow
[new Set(['1']), StreamingState.Idle, false], // Not constraining height
- [new Set(['1']), StreamingState.Responding, true], // Streaming
])(
'renders nothing when: overflow=%s, streaming=%s, constrain=%s',
async (overflowingIds, streamingState, constrainHeight) => {
@@ -46,9 +45,28 @@ describe('ShowMoreLines', () => {
},
);
- it.each([[StreamingState.Idle], [StreamingState.WaitingForConfirmation]])(
- 'renders message when overflowing and state is %s',
+ it('renders nothing in STANDARD mode even if overflowing', async () => {
+ mockUseAlternateBuffer.mockReturnValue(false);
+ mockUseOverflowState.mockReturnValue({
+ overflowingIds: new Set(['1']),
+ } as NonNullable>);
+ mockUseStreamingContext.mockReturnValue(StreamingState.Idle);
+ const { lastFrame, waitUntilReady, unmount } = render(
+ ,
+ );
+ await waitUntilReady();
+ expect(lastFrame({ allowEmpty: true })).toBe('');
+ unmount();
+ });
+
+ it.each([
+ [StreamingState.Idle],
+ [StreamingState.WaitingForConfirmation],
+ [StreamingState.Responding],
+ ])(
+ 'renders message in ASB mode when overflowing and state is %s',
async (streamingState) => {
+ mockUseAlternateBuffer.mockReturnValue(true);
mockUseOverflowState.mockReturnValue({
overflowingIds: new Set(['1']),
} as NonNullable>);
@@ -57,8 +75,39 @@ describe('ShowMoreLines', () => {
,
);
await waitUntilReady();
- expect(lastFrame()).toContain('Press ctrl-o to show more lines');
+ expect(lastFrame().toLowerCase()).toContain(
+ 'press ctrl+o to show more lines',
+ );
unmount();
},
);
+
+ it('renders message in ASB mode when isOverflowing prop is true even if internal overflow state is empty', async () => {
+ mockUseAlternateBuffer.mockReturnValue(true);
+ mockUseOverflowState.mockReturnValue({
+ overflowingIds: new Set(),
+ } as NonNullable>);
+ mockUseStreamingContext.mockReturnValue(StreamingState.Idle);
+ const { lastFrame, waitUntilReady, unmount } = render(
+ ,
+ );
+ await waitUntilReady();
+ expect(lastFrame().toLowerCase()).toContain(
+ 'press ctrl+o to show more lines',
+ );
+ unmount();
+ });
+
+ it('renders nothing when isOverflowing prop is false even if internal overflow state has IDs', async () => {
+ mockUseOverflowState.mockReturnValue({
+ overflowingIds: new Set(['1']),
+ } as NonNullable>);
+ mockUseStreamingContext.mockReturnValue(StreamingState.Idle);
+ const { lastFrame, waitUntilReady, unmount } = render(
+ ,
+ );
+ await waitUntilReady();
+ expect(lastFrame({ allowEmpty: true })).toBe('');
+ unmount();
+ });
});
diff --git a/packages/cli/src/ui/components/ShowMoreLines.tsx b/packages/cli/src/ui/components/ShowMoreLines.tsx
index a3317d4dc60..92acd2b29a7 100644
--- a/packages/cli/src/ui/components/ShowMoreLines.tsx
+++ b/packages/cli/src/ui/components/ShowMoreLines.tsx
@@ -9,31 +9,42 @@ import { useOverflowState } from '../contexts/OverflowContext.js';
import { useStreamingContext } from '../contexts/StreamingContext.js';
import { StreamingState } from '../types.js';
import { theme } from '../semantic-colors.js';
+import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
interface ShowMoreLinesProps {
constrainHeight: boolean;
+ isOverflowing?: boolean;
}
-export const ShowMoreLines = ({ constrainHeight }: ShowMoreLinesProps) => {
+export const ShowMoreLines = ({
+ constrainHeight,
+ isOverflowing: isOverflowingProp,
+}: ShowMoreLinesProps) => {
+ const isAlternateBuffer = useAlternateBuffer();
const overflowState = useOverflowState();
const streamingState = useStreamingContext();
+ const isOverflowing =
+ isOverflowingProp ??
+ (overflowState !== undefined && overflowState.overflowingIds.size > 0);
+
if (
- overflowState === undefined ||
- overflowState.overflowingIds.size === 0 ||
+ !isAlternateBuffer ||
+ !isOverflowing ||
!constrainHeight ||
!(
streamingState === StreamingState.Idle ||
- streamingState === StreamingState.WaitingForConfirmation
+ streamingState === StreamingState.WaitingForConfirmation ||
+ streamingState === StreamingState.Responding
)
) {
return null;
}
return (
-
-
- Press ctrl-o to show more lines
+
+
+ Press Ctrl+O to show more lines
);
diff --git a/packages/cli/src/ui/components/ToastDisplay.test.tsx b/packages/cli/src/ui/components/ToastDisplay.test.tsx
index da509992047..f2ef9a287b0 100644
--- a/packages/cli/src/ui/components/ToastDisplay.test.tsx
+++ b/packages/cli/src/ui/components/ToastDisplay.test.tsx
@@ -35,12 +35,22 @@ describe('ToastDisplay', () => {
buffer: { text: '' } as TextBuffer,
history: [] as HistoryItem[],
queueErrorMessage: null,
+ showIsExpandableHint: false,
};
it('returns false for default state', () => {
expect(shouldShowToast(baseState as UIState)).toBe(false);
});
+ it('returns true when showIsExpandableHint is true', () => {
+ expect(
+ shouldShowToast({
+ ...baseState,
+ showIsExpandableHint: true,
+ } as UIState),
+ ).toBe(true);
+ });
+
it('returns true when ctrlCPressedOnce is true', () => {
expect(
shouldShowToast({ ...baseState, ctrlCPressedOnce: true } as UIState),
@@ -170,4 +180,22 @@ describe('ToastDisplay', () => {
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
});
+
+ it('renders expansion hint when showIsExpandableHint is true', async () => {
+ const { lastFrame, waitUntilReady } = renderToastDisplay({
+ showIsExpandableHint: true,
+ constrainHeight: true,
+ });
+ await waitUntilReady();
+ expect(lastFrame()).toContain('Press Ctrl+O to show more lines');
+ });
+
+ it('renders collapse hint when showIsExpandableHint is true and constrainHeight is false', async () => {
+ const { lastFrame, waitUntilReady } = renderToastDisplay({
+ showIsExpandableHint: true,
+ constrainHeight: false,
+ });
+ await waitUntilReady();
+ expect(lastFrame()).toContain('Press Ctrl+O to collapse lines');
+ });
});
diff --git a/packages/cli/src/ui/components/ToastDisplay.tsx b/packages/cli/src/ui/components/ToastDisplay.tsx
index 37d2997e336..e3832012195 100644
--- a/packages/cli/src/ui/components/ToastDisplay.tsx
+++ b/packages/cli/src/ui/components/ToastDisplay.tsx
@@ -17,7 +17,8 @@ export function shouldShowToast(uiState: UIState): boolean {
uiState.ctrlDPressedOnce ||
(uiState.showEscapePrompt &&
(uiState.buffer.text.length > 0 || uiState.history.length > 0)) ||
- Boolean(uiState.queueErrorMessage)
+ Boolean(uiState.queueErrorMessage) ||
+ uiState.showIsExpandableHint
);
}
@@ -73,5 +74,14 @@ export const ToastDisplay: React.FC = () => {
return {uiState.queueErrorMessage};
}
+ if (uiState.showIsExpandableHint) {
+ const action = uiState.constrainHeight ? 'show more' : 'collapse';
+ return (
+
+ Press Ctrl+O to {action} lines for the most recent response
+
+ );
+ }
+
return null;
};
diff --git a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx
index 345d00a263f..ab7d080b370 100644
--- a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx
+++ b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx
@@ -161,7 +161,7 @@ describe('ToolConfirmationQueue', () => {
,
{
config: mockConfig,
- useAlternateBuffer: false,
+ useAlternateBuffer: true,
uiState: {
terminalWidth: 80,
terminalHeight: 20,
@@ -173,10 +173,11 @@ describe('ToolConfirmationQueue', () => {
await waitUntilReady();
await waitFor(() =>
- expect(lastFrame()).toContain('Press ctrl-o to show more lines'),
+ expect(lastFrame()?.toLowerCase()).toContain(
+ 'press ctrl+o to show more lines',
+ ),
);
expect(lastFrame()).toMatchSnapshot();
- expect(lastFrame()).toContain('Press ctrl-o to show more lines');
unmount();
});
@@ -324,7 +325,7 @@ describe('ToolConfirmationQueue', () => {
await waitUntilReady();
const output = lastFrame();
- expect(output).not.toContain('Press ctrl-o to show more lines');
+ expect(output).not.toContain('Press CTRL-O to show more lines');
expect(output).toMatchSnapshot();
unmount();
});
diff --git a/packages/cli/src/ui/components/ToolConfirmationQueue.tsx b/packages/cli/src/ui/components/ToolConfirmationQueue.tsx
index e3c18e0231b..c89c98f8d49 100644
--- a/packages/cli/src/ui/components/ToolConfirmationQueue.tsx
+++ b/packages/cli/src/ui/components/ToolConfirmationQueue.tsx
@@ -71,13 +71,12 @@ export const ToolConfirmationQueue: React.FC = ({
// - 2 lines for the rounded border
// - 2 lines for the Header (text + margin)
// - 2 lines for Tool Identity (text + margin)
- const availableContentHeight =
- constrainHeight && !isAlternateBuffer
- ? Math.max(maxHeight - (hideToolIdentity ? 4 : 6), 4)
- : undefined;
+ const availableContentHeight = constrainHeight
+ ? Math.max(maxHeight - (hideToolIdentity ? 4 : 6), 4)
+ : undefined;
- return (
-
+ const content = (
+ <>
= ({
/>
-
+ >
+ );
+
+ return isAlternateBuffer ? (
+ /* Shadow the global provider to maintain isolation in ASB mode. */
+ {content}
+ ) : (
+ content
);
};
diff --git a/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap
index 3da5a05c0c5..8fb49b8b71f 100644
--- a/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/AlternateBufferQuittingDisplay.test.tsx.snap
@@ -43,7 +43,6 @@ Tips for getting started:
│ ✓ tool1 Description for tool 1 │
│ │
╰──────────────────────────────────────────────────────────────────────────╯
-
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool2 Description for tool 2 │
│ │
@@ -90,7 +89,6 @@ Tips for getting started:
│ ✓ tool1 Description for tool 1 │
│ │
╰──────────────────────────────────────────────────────────────────────────╯
-
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool2 Description for tool 2 │
│ │
diff --git a/packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap
index 28929deee56..8d03baaa498 100644
--- a/packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/ConfigInitDisplay.test.tsx.snap
@@ -18,20 +18,8 @@ Spinner Connecting to MCP servers... (0/5) - Waiting for: s1, s2, s3, +2 more
"
`;
-exports[`ConfigInitDisplay > truncates list of waiting servers if too many 2`] = `
-"
-Spinner Connecting to MCP servers... (0/5) - Waiting for: s1, s2, s3, +2 more
-"
-`;
-
exports[`ConfigInitDisplay > updates message on McpClientUpdate event 1`] = `
"
Spinner Connecting to MCP servers... (1/2) - Waiting for: server2
"
`;
-
-exports[`ConfigInitDisplay > updates message on McpClientUpdate event 2`] = `
-"
-Spinner Connecting to MCP servers... (1/2) - Waiting for: server2
-"
-`;
diff --git a/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap
index faa759a0502..587ded8f299 100644
--- a/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap
@@ -27,33 +27,6 @@ Enter to select · ↑/↓ to navigate · Esc to cancel
"
`;
-exports[`ExitPlanModeDialog > useAlternateBuffer: false > bubbles up Ctrl+C when feedback is empty while editing 2`] = `
-"Overview
-
-Add user authentication to the CLI application.
-
-Implementation Steps
-
- 1. Create src/auth/AuthService.ts with login/logout methods
- 2. Add session storage in src/storage/SessionStore.ts
- 3. Update src/commands/index.ts to check auth status
- 4. Add tests in src/auth/__tests__/
-
-Files to Modify
-
- - src/index.ts - Add auth middleware
- - src/config.ts - Add auth configuration options
-
- 1. Yes, automatically accept edits
- Approves plan and allows tools to run automatically
- 2. Yes, manually accept edits
- Approves plan but requires confirmation for each tool
-● 3. Type your feedback...
-
-Enter to submit · Esc to cancel
-"
-`;
-
exports[`ExitPlanModeDialog > useAlternateBuffer: false > calls onFeedback when feedback is typed and submitted 1`] = `
"Overview
@@ -81,33 +54,6 @@ Enter to select · ↑/↓ to navigate · Esc to cancel
"
`;
-exports[`ExitPlanModeDialog > useAlternateBuffer: false > calls onFeedback when feedback is typed and submitted 2`] = `
-"Overview
-
-Add user authentication to the CLI application.
-
-Implementation Steps
-
- 1. Create src/auth/AuthService.ts with login/logout methods
- 2. Add session storage in src/storage/SessionStore.ts
- 3. Update src/commands/index.ts to check auth status
- 4. Add tests in src/auth/__tests__/
-
-Files to Modify
-
- - src/index.ts - Add auth middleware
- - src/config.ts - Add auth configuration options
-
- 1. Yes, automatically accept edits
- Approves plan and allows tools to run automatically
- 2. Yes, manually accept edits
- Approves plan but requires confirmation for each tool
-● 3. Add tests
-
-Enter to submit · Esc to cancel
-"
-`;
-
exports[`ExitPlanModeDialog > useAlternateBuffer: false > displays error state when file read fails 1`] = `
" Error reading plan: File not found
"
@@ -194,33 +140,6 @@ Enter to select · ↑/↓ to navigate · Esc to cancel
"
`;
-exports[`ExitPlanModeDialog > useAlternateBuffer: true > bubbles up Ctrl+C when feedback is empty while editing 2`] = `
-"Overview
-
-Add user authentication to the CLI application.
-
-Implementation Steps
-
- 1. Create src/auth/AuthService.ts with login/logout methods
- 2. Add session storage in src/storage/SessionStore.ts
- 3. Update src/commands/index.ts to check auth status
- 4. Add tests in src/auth/__tests__/
-
-Files to Modify
-
- - src/index.ts - Add auth middleware
- - src/config.ts - Add auth configuration options
-
- 1. Yes, automatically accept edits
- Approves plan and allows tools to run automatically
- 2. Yes, manually accept edits
- Approves plan but requires confirmation for each tool
-● 3. Type your feedback...
-
-Enter to submit · Esc to cancel
-"
-`;
-
exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 1`] = `
"Overview
@@ -248,33 +167,6 @@ Enter to select · ↑/↓ to navigate · Esc to cancel
"
`;
-exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 2`] = `
-"Overview
-
-Add user authentication to the CLI application.
-
-Implementation Steps
-
- 1. Create src/auth/AuthService.ts with login/logout methods
- 2. Add session storage in src/storage/SessionStore.ts
- 3. Update src/commands/index.ts to check auth status
- 4. Add tests in src/auth/__tests__/
-
-Files to Modify
-
- - src/index.ts - Add auth middleware
- - src/config.ts - Add auth configuration options
-
- 1. Yes, automatically accept edits
- Approves plan and allows tools to run automatically
- 2. Yes, manually accept edits
- Approves plan but requires confirmation for each tool
-● 3. Add tests
-
-Enter to submit · Esc to cancel
-"
-`;
-
exports[`ExitPlanModeDialog > useAlternateBuffer: true > displays error state when file read fails 1`] = `
" Error reading plan: File not found
"
diff --git a/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap
index 29eda04babb..c7a1d0f48b1 100644
--- a/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap
@@ -6,27 +6,17 @@ AppHeader(full)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊷ Shell Command Running a long command... │
│ │
-│ Line 1 │
-│ Line 2 │
-│ Line 3 │
-│ Line 4 │
-│ Line 5 │
-│ Line 6 │
-│ Line 7 │
-│ Line 8 │
-│ Line 9 │
│ Line 10 │
│ Line 11 │
│ Line 12 │
│ Line 13 │
│ Line 14 │
-│ Line 15 │
-│ Line 16 │
-│ Line 17 │
-│ Line 18 │
-│ Line 19 │
-│ Line 20 │
-│ │
+│ Line 15 █ │
+│ Line 16 █ │
+│ Line 17 █ │
+│ Line 18 █ │
+│ Line 19 █ │
+│ Line 20 █ │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
ShowMoreLines
"
@@ -38,15 +28,11 @@ AppHeader(full)
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊷ Shell Command Running a long command... │
│ │
-│ Line 6 │
-│ Line 7 │
-│ Line 8 │
-│ Line 9 ▄ │
-│ Line 10 █ │
-│ Line 11 █ │
-│ Line 12 █ │
-│ Line 13 █ │
-│ Line 14 █ │
+│ Line 10 │
+│ Line 11 │
+│ Line 12 │
+│ Line 13 │
+│ Line 14 │
│ Line 15 █ │
│ Line 16 █ │
│ Line 17 █ │
@@ -63,12 +49,7 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Con
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊷ Shell Command Running a long command... │
│ │
-│ Line 6 │
-│ Line 7 │
-│ Line 8 │
-│ Line 9 │
-│ Line 10 │
-│ Line 11 │
+│ ... first 11 lines hidden ... │
│ Line 12 │
│ Line 13 │
│ Line 14 │
@@ -88,6 +69,11 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unc
╭──────────────────────────────────────────────────────────────────────────────────────────────╮
│ ⊷ Shell Command Running a long command... │
│ │
+│ Line 1 │
+│ Line 2 │
+│ Line 3 │
+│ Line 4 │
+│ Line 5 │
│ Line 6 │
│ Line 7 │
│ Line 8 │
diff --git a/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue.test.tsx.snap
index 56dcb64d707..b5e013ef487 100644
--- a/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue.test.tsx.snap
@@ -16,7 +16,6 @@ exports[`ToolConfirmationQueue > calculates availableContentHeight based on avai
│ 4. No, suggest changes (esc) │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
- Press ctrl-o to show more lines
"
`;
@@ -107,7 +106,7 @@ exports[`ToolConfirmationQueue > renders expansion hint when content is long and
│ 4. No, suggest changes (esc) │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
- Press ctrl-o to show more lines
+ Press Ctrl+O to show more lines
"
`;
diff --git a/packages/cli/src/ui/components/messages/GeminiMessage.tsx b/packages/cli/src/ui/components/messages/GeminiMessage.tsx
index 3c17a3850f6..0bdf9b65e94 100644
--- a/packages/cli/src/ui/components/messages/GeminiMessage.tsx
+++ b/packages/cli/src/ui/components/messages/GeminiMessage.tsx
@@ -12,6 +12,7 @@ import { theme } from '../../semantic-colors.js';
import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js';
import { useUIState } from '../../contexts/UIStateContext.js';
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
+import { OverflowProvider } from '../../contexts/OverflowContext.js';
interface GeminiMessageProps {
text: string;
@@ -31,7 +32,7 @@ export const GeminiMessage: React.FC = ({
const prefixWidth = prefix.length;
const isAlternateBuffer = useAlternateBuffer();
- return (
+ const content = (
@@ -61,4 +62,11 @@ export const GeminiMessage: React.FC = ({
);
+
+ return isAlternateBuffer ? (
+ /* Shadow the global provider to maintain isolation in ASB mode. */
+ {content}
+ ) : (
+ content
+ );
};
diff --git a/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx
index 6359b5b250b..72ce8cec5f1 100644
--- a/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx
+++ b/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx
@@ -191,7 +191,7 @@ describe('', () => {
true,
],
[
- 'defaults to ACTIVE_SHELL_MAX_LINES when availableTerminalHeight is undefined',
+ 'defaults to ACTIVE_SHELL_MAX_LINES in alternate buffer when availableTerminalHeight is undefined',
undefined,
ACTIVE_SHELL_MAX_LINES,
false,
@@ -219,5 +219,75 @@ describe('', () => {
expect(frame.match(/Line \d+/g)?.length).toBe(expectedMaxLines);
expect(frame).toMatchSnapshot();
});
+
+ it('fully expands in standard mode when availableTerminalHeight is undefined', async () => {
+ const { lastFrame } = renderShell(
+ {
+ resultDisplay: LONG_OUTPUT,
+ renderOutputAsMarkdown: false,
+ availableTerminalHeight: undefined,
+ status: CoreToolCallStatus.Executing,
+ },
+ { useAlternateBuffer: false },
+ );
+
+ await waitFor(() => {
+ const frame = lastFrame();
+ // Should show all 100 lines
+ expect(frame.match(/Line \d+/g)?.length).toBe(100);
+ });
+ });
+
+ it('fully expands in alternate buffer mode when constrainHeight is false and isExpandable is true', async () => {
+ const { lastFrame, waitUntilReady } = renderShell(
+ {
+ resultDisplay: LONG_OUTPUT,
+ renderOutputAsMarkdown: false,
+ availableTerminalHeight: undefined,
+ status: CoreToolCallStatus.Success,
+ isExpandable: true,
+ },
+ {
+ useAlternateBuffer: true,
+ uiState: {
+ constrainHeight: false,
+ },
+ },
+ );
+
+ await waitUntilReady();
+ await waitFor(() => {
+ const frame = lastFrame();
+ // Should show all 100 lines because constrainHeight is false and isExpandable is true
+ expect(frame.match(/Line \d+/g)?.length).toBe(100);
+ });
+ expect(lastFrame()).toMatchSnapshot();
+ });
+
+ it('stays constrained in alternate buffer mode when isExpandable is false even if constrainHeight is false', async () => {
+ const { lastFrame, waitUntilReady } = renderShell(
+ {
+ resultDisplay: LONG_OUTPUT,
+ renderOutputAsMarkdown: false,
+ availableTerminalHeight: undefined,
+ status: CoreToolCallStatus.Success,
+ isExpandable: false,
+ },
+ {
+ useAlternateBuffer: true,
+ uiState: {
+ constrainHeight: false,
+ },
+ },
+ );
+
+ await waitUntilReady();
+ await waitFor(() => {
+ const frame = lastFrame();
+ // Should still be constrained to ACTIVE_SHELL_MAX_LINES (15) because isExpandable is false
+ expect(frame.match(/Line \d+/g)?.length).toBe(15);
+ });
+ expect(lastFrame()).toMatchSnapshot();
+ });
});
});
diff --git a/packages/cli/src/ui/components/messages/ShellToolMessage.tsx b/packages/cli/src/ui/components/messages/ShellToolMessage.tsx
index 50af3bc1e66..54abbc09d3a 100644
--- a/packages/cli/src/ui/components/messages/ShellToolMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ShellToolMessage.tsx
@@ -15,24 +15,21 @@ import {
ToolStatusIndicator,
ToolInfo,
TrailingIndicator,
- STATUS_INDICATOR_WIDTH,
isThisShellFocusable as checkIsShellFocusable,
isThisShellFocused as checkIsShellFocused,
useFocusHint,
FocusHint,
} from './ToolShared.js';
import type { ToolMessageProps } from './ToolMessage.js';
-import {
- ACTIVE_SHELL_MAX_LINES,
- COMPLETED_SHELL_MAX_LINES,
-} from '../../constants.js';
+import { ACTIVE_SHELL_MAX_LINES } from '../../constants.js';
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
-import { type Config, CoreToolCallStatus } from '@google/gemini-cli-core';
-
import { useUIState } from '../../contexts/UIStateContext.js';
+import { type Config } from '@google/gemini-cli-core';
+import { calculateShellMaxLines } from '../../utils/toolLayoutUtils.js';
export interface ShellToolMessageProps extends ToolMessageProps {
config?: Config;
+ isExpandable?: boolean;
}
export const ShellToolMessage: React.FC = ({
@@ -61,9 +58,15 @@ export const ShellToolMessage: React.FC = ({
borderColor,
borderDimColor,
+ isExpandable,
}) => {
- const { activePtyId: activeShellPtyId, embeddedShellFocused } = useUIState();
+ const {
+ activePtyId: activeShellPtyId,
+ embeddedShellFocused,
+ constrainHeight,
+ } = useUIState();
const isAlternateBuffer = useAlternateBuffer();
+
const isThisShellFocused = checkIsShellFocused(
name,
status,
@@ -155,59 +158,23 @@ export const ShellToolMessage: React.FC = ({
terminalWidth={terminalWidth}
renderOutputAsMarkdown={renderOutputAsMarkdown}
hasFocus={isThisShellFocused}
- maxLines={getShellMaxLines(
+ maxLines={calculateShellMaxLines({
status,
isAlternateBuffer,
isThisShellFocused,
availableTerminalHeight,
- )}
+ constrainHeight,
+ isExpandable,
+ })}
/>
{isThisShellFocused && config && (
-
-
-
+
)}
>
);
};
-
-/**
- * Calculates the maximum number of lines to display for shell output.
- *
- * For completed processes (Success, Error, Canceled), it returns COMPLETED_SHELL_MAX_LINES.
- * For active processes, it returns the available terminal height if in alternate buffer mode
- * and focused. Otherwise, it returns ACTIVE_SHELL_MAX_LINES.
- *
- * This function ensures a finite number of lines is always returned to prevent performance issues.
- */
-function getShellMaxLines(
- status: CoreToolCallStatus,
- isAlternateBuffer: boolean,
- isThisShellFocused: boolean,
- availableTerminalHeight: number | undefined,
-): number {
- if (
- status === CoreToolCallStatus.Success ||
- status === CoreToolCallStatus.Error ||
- status === CoreToolCallStatus.Cancelled
- ) {
- return COMPLETED_SHELL_MAX_LINES;
- }
-
- if (availableTerminalHeight === undefined) {
- return ACTIVE_SHELL_MAX_LINES;
- }
-
- const maxLinesBasedOnHeight = Math.max(1, availableTerminalHeight - 2);
-
- if (isAlternateBuffer && isThisShellFocused) {
- return maxLinesBasedOnHeight;
- }
-
- return Math.min(maxLinesBasedOnHeight, ACTIVE_SHELL_MAX_LINES);
-}
diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx
index b3e0275a1bd..1ead1503e59 100644
--- a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx
@@ -6,6 +6,7 @@
import { renderWithProviders } from '../../../test-utils/render.js';
import { describe, it, expect, vi, afterEach } from 'vitest';
+import { act } from 'react';
import { ToolGroupMessage } from './ToolGroupMessage.js';
import type {
HistoryItem,
@@ -678,4 +679,194 @@ describe('', () => {
},
);
});
+
+ describe('Manual Overflow Detection', () => {
+ it('detects overflow for string results exceeding available height', async () => {
+ const toolCalls = [
+ createToolCall({
+ resultDisplay: 'line 1\nline 2\nline 3\nline 4\nline 5',
+ }),
+ ];
+ const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
+ ,
+ {
+ config: baseMockConfig,
+ useAlternateBuffer: true,
+ uiState: {
+ constrainHeight: true,
+ },
+ },
+ );
+ await waitUntilReady();
+ expect(lastFrame()?.toLowerCase()).toContain(
+ 'press ctrl+o to show more lines',
+ );
+ unmount();
+ });
+
+ it('detects overflow for array results exceeding available height', async () => {
+ // resultDisplay when array is expected to be AnsiLine[]
+ // AnsiLine is AnsiToken[]
+ const toolCalls = [
+ createToolCall({
+ resultDisplay: Array(5).fill([{ text: 'line', fg: 'default' }]),
+ }),
+ ];
+ const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
+ ,
+ {
+ config: baseMockConfig,
+ useAlternateBuffer: true,
+ uiState: {
+ constrainHeight: true,
+ },
+ },
+ );
+ await waitUntilReady();
+ expect(lastFrame()?.toLowerCase()).toContain(
+ 'press ctrl+o to show more lines',
+ );
+ unmount();
+ });
+
+ it('respects ACTIVE_SHELL_MAX_LINES for focused shell tools', async () => {
+ const toolCalls = [
+ createToolCall({
+ name: 'run_shell_command',
+ status: CoreToolCallStatus.Executing,
+ ptyId: 1,
+ resultDisplay: Array(20).fill('line').join('\n'), // 20 lines > 15 (limit)
+ }),
+ ];
+ const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
+ ,
+ {
+ config: baseMockConfig,
+ useAlternateBuffer: true,
+ uiState: {
+ constrainHeight: true,
+ activePtyId: 1,
+ embeddedShellFocused: true,
+ },
+ },
+ );
+ await waitUntilReady();
+ expect(lastFrame()?.toLowerCase()).toContain(
+ 'press ctrl+o to show more lines',
+ );
+ unmount();
+ });
+
+ it('does not show expansion hint when content is within limits', async () => {
+ const toolCalls = [
+ createToolCall({
+ resultDisplay: 'small result',
+ }),
+ ];
+ const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
+ ,
+ {
+ config: baseMockConfig,
+ useAlternateBuffer: true,
+ uiState: {
+ constrainHeight: true,
+ },
+ },
+ );
+ await waitUntilReady();
+ expect(lastFrame()).not.toContain('Press Ctrl+O to show more lines');
+ unmount();
+ });
+
+ it('hides expansion hint when constrainHeight is false', async () => {
+ const toolCalls = [
+ createToolCall({
+ resultDisplay: 'line 1\nline 2\nline 3\nline 4\nline 5',
+ }),
+ ];
+ const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
+ ,
+ {
+ config: baseMockConfig,
+ useAlternateBuffer: true,
+ uiState: {
+ constrainHeight: false,
+ },
+ },
+ );
+ await waitUntilReady();
+ expect(lastFrame()).not.toContain('Press Ctrl+O to show more lines');
+ unmount();
+ });
+
+ it('isolates overflow hint in ASB mode (ignores global overflow state)', async () => {
+ // In this test, the tool output is SHORT (no local overflow).
+ // We will inject a dummy ID into the global overflow state.
+ // ToolGroupMessage should still NOT show the hint because it calculates
+ // overflow locally and passes it as a prop.
+ const toolCalls = [
+ createToolCall({
+ resultDisplay: 'short result',
+ }),
+ ];
+ const { lastFrame, unmount, waitUntilReady, capturedOverflowActions } =
+ renderWithProviders(
+ ,
+ {
+ config: baseMockConfig,
+ useAlternateBuffer: true,
+ uiState: {
+ constrainHeight: true,
+ },
+ },
+ );
+ await waitUntilReady();
+
+ // Manually trigger a global overflow
+ act(() => {
+ expect(capturedOverflowActions).toBeDefined();
+ capturedOverflowActions!.addOverflowingId('unrelated-global-id');
+ });
+
+ // The hint should NOT appear because ToolGroupMessage is isolated by its prop logic
+ expect(lastFrame()).not.toContain('Press Ctrl+O to show more lines');
+ unmount();
+ });
+ });
});
diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
index f4e1c200db5..3c3dcf56d32 100644
--- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
@@ -17,10 +17,15 @@ import { ToolMessage } from './ToolMessage.js';
import { ShellToolMessage } from './ShellToolMessage.js';
import { theme } from '../../semantic-colors.js';
import { useConfig } from '../../contexts/ConfigContext.js';
-import { isShellTool } from './ToolShared.js';
+import { isShellTool, isThisShellFocused } from './ToolShared.js';
import { shouldHideToolCall } from '@google/gemini-cli-core';
import { ShowMoreLines } from '../ShowMoreLines.js';
import { useUIState } from '../../contexts/UIStateContext.js';
+import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
+import {
+ calculateShellMaxLines,
+ calculateToolContentMaxLines,
+} from '../../utils/toolLayoutUtils.js';
import { getToolGroupBorderAppearance } from '../../utils/borderStyles.js';
interface ToolGroupMessageProps {
@@ -31,6 +36,7 @@ interface ToolGroupMessageProps {
onShellInputSubmit?: (input: string) => void;
borderTop?: boolean;
borderBottom?: boolean;
+ isExpandable?: boolean;
}
// Main component renders the border and maps the tools using ToolMessage
@@ -43,6 +49,7 @@ export const ToolGroupMessage: React.FC = ({
terminalWidth,
borderTop: borderTopOverride,
borderBottom: borderBottomOverride,
+ isExpandable,
}) => {
// Filter out tool calls that should be hidden (e.g. in-progress Ask User, or Plan Mode operations).
const toolCalls = useMemo(
@@ -67,6 +74,7 @@ export const ToolGroupMessage: React.FC = ({
backgroundShells,
pendingHistoryItems,
} = useUIState();
+ const isAlternateBuffer = useAlternateBuffer();
const { borderColor, borderDimColor } = useMemo(
() =>
@@ -106,14 +114,6 @@ export const ToolGroupMessage: React.FC = ({
const staticHeight = /* border */ 2;
- // If all tools are filtered out (e.g., in-progress AskUser tools, confirming tools),
- // only render if we need to close a border from previous
- // tool groups. borderBottomOverride=true means we must render the closing border;
- // undefined or false means there's nothing to display.
- if (visibleToolCalls.length === 0 && borderBottomOverride !== true) {
- return null;
- }
-
let countToolCallsWithResults = 0;
for (const tool of visibleToolCalls) {
if (tool.resultDisplay !== undefined && tool.resultDisplay !== '') {
@@ -134,21 +134,91 @@ export const ToolGroupMessage: React.FC = ({
const contentWidth = terminalWidth - TOOL_MESSAGE_HORIZONTAL_MARGIN;
- return (
- // This box doesn't have a border even though it conceptually does because
- // we need to allow the sticky headers to render the borders themselves so
- // that the top border can be sticky.
+ /*
+ * ToolGroupMessage calculates its own overflow state locally and passes
+ * it as a prop to ShowMoreLines. This isolates it from global overflow
+ * reports in ASB mode, while allowing it to contribute to the global
+ * 'Toast' hint in Standard mode.
+ *
+ * Because of this prop-based isolation and the explicit mode-checks in
+ * AppContainer, we do not need to shadow the OverflowProvider here.
+ */
+ const hasOverflow = useMemo(() => {
+ if (!availableTerminalHeightPerToolMessage) return false;
+ return visibleToolCalls.some((tool) => {
+ const isShellToolCall = isShellTool(tool.name);
+ const isFocused = isThisShellFocused(
+ tool.name,
+ tool.status,
+ tool.ptyId,
+ activePtyId,
+ embeddedShellFocused,
+ );
+
+ let maxLines: number | undefined;
+
+ if (isShellToolCall) {
+ maxLines = calculateShellMaxLines({
+ status: tool.status,
+ isAlternateBuffer,
+ isThisShellFocused: isFocused,
+ availableTerminalHeight: availableTerminalHeightPerToolMessage,
+ constrainHeight,
+ isExpandable,
+ });
+ }
+
+ // Standard tools and Shell tools both eventually use ToolResultDisplay's logic.
+ // ToolResultDisplay uses calculateToolContentMaxLines to find the final line budget.
+ const contentMaxLines = calculateToolContentMaxLines({
+ availableTerminalHeight: availableTerminalHeightPerToolMessage,
+ isAlternateBuffer,
+ maxLinesLimit: maxLines,
+ });
+
+ if (!contentMaxLines) return false;
+
+ if (typeof tool.resultDisplay === 'string') {
+ const text = tool.resultDisplay;
+ const hasTrailingNewline = text.endsWith('\n');
+ const contentText = hasTrailingNewline ? text.slice(0, -1) : text;
+ const lineCount = contentText.split('\n').length;
+ return lineCount > contentMaxLines;
+ }
+ if (Array.isArray(tool.resultDisplay)) {
+ return tool.resultDisplay.length > contentMaxLines;
+ }
+ return false;
+ });
+ }, [
+ visibleToolCalls,
+ availableTerminalHeightPerToolMessage,
+ activePtyId,
+ embeddedShellFocused,
+ isAlternateBuffer,
+ constrainHeight,
+ isExpandable,
+ ]);
+
+ // If all tools are filtered out (e.g., in-progress AskUser tools, confirming tools),
+ // only render if we need to close a border from previous
+ // tool groups. borderBottomOverride=true means we must render the closing border;
+ // undefined or false means there's nothing to display.
+ if (visibleToolCalls.length === 0 && borderBottomOverride !== true) {
+ return null;
+ }
+
+ const content = (
{visibleToolCalls.map((tool, index) => {
const isFirst = index === 0;
@@ -165,6 +235,7 @@ export const ToolGroupMessage: React.FC = ({
: isFirst,
borderColor,
borderDimColor,
+ isExpandable,
};
return (
@@ -179,34 +250,34 @@ export const ToolGroupMessage: React.FC = ({
) : (
)}
-
- {tool.outputFile && (
+ {tool.outputFile && (
+
Output too long and was saved to: {tool.outputFile}
- )}
-
+
+ )}
);
})}
{
/*
- We have to keep the bottom border separate so it doesn't get
- drawn over by the sticky header directly inside it.
- */
+ We have to keep the bottom border separate so it doesn't get
+ drawn over by the sticky header directly inside it.
+ */
(visibleToolCalls.length > 0 || borderBottomOverride !== undefined) && (
= ({
)
}
{(borderBottomOverride ?? true) && visibleToolCalls.length > 0 && (
-
+
)}
);
+
+ return content;
};
diff --git a/packages/cli/src/ui/components/messages/ToolOverflowConsistencyChecks.test.tsx b/packages/cli/src/ui/components/messages/ToolOverflowConsistencyChecks.test.tsx
new file mode 100644
index 00000000000..f7629945d9c
--- /dev/null
+++ b/packages/cli/src/ui/components/messages/ToolOverflowConsistencyChecks.test.tsx
@@ -0,0 +1,115 @@
+/**
+ * @license
+ * Copyright 2026 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect } from 'vitest';
+import { ToolGroupMessage } from './ToolGroupMessage.js';
+import { renderWithProviders } from '../../../test-utils/render.js';
+import { StreamingState, type IndividualToolCallDisplay } from '../../types.js';
+import { OverflowProvider } from '../../contexts/OverflowContext.js';
+import { waitFor } from '../../../test-utils/async.js';
+import { CoreToolCallStatus } from '@google/gemini-cli-core';
+
+describe('ToolOverflowConsistencyChecks: ToolGroupMessage and ToolResultDisplay synchronization', () => {
+ it('should ensure explicit hasOverflow calculation is consistent with ToolResultDisplay truncation in Alternate Buffer (ASB) mode', async () => {
+ /**
+ * Logic:
+ * 1. availableTerminalHeight(13) - staticHeight(3) = 10 lines per tool.
+ * 2. ASB mode reserves 1 + 6 = 7 lines.
+ * 3. Line budget = 10 - 7 = 3 lines.
+ * 4. 5 lines of output > 3 lines budget => hasOverflow should be TRUE.
+ */
+
+ const lines = Array.from({ length: 5 }, (_, i) => `line ${i + 1}`);
+ const resultDisplay = lines.join('\n');
+
+ const toolCalls: IndividualToolCallDisplay[] = [
+ {
+ callId: 'call-1',
+ name: 'test-tool',
+ description: 'a test tool',
+ status: CoreToolCallStatus.Success,
+ resultDisplay,
+ confirmationDetails: undefined,
+ },
+ ];
+
+ const { lastFrame } = renderWithProviders(
+
+
+ ,
+ {
+ uiState: {
+ streamingState: StreamingState.Idle,
+ constrainHeight: true,
+ },
+ useAlternateBuffer: true,
+ },
+ );
+
+ // In ASB mode, the hint should appear because hasOverflow is now correctly calculated.
+ await waitFor(() =>
+ expect(lastFrame()?.toLowerCase()).toContain(
+ 'press ctrl+o to show more lines',
+ ),
+ );
+ });
+
+ it('should ensure explicit hasOverflow calculation is consistent with ToolResultDisplay truncation in Standard mode', async () => {
+ /**
+ * Logic:
+ * 1. availableTerminalHeight(13) - staticHeight(3) = 10 lines per tool.
+ * 2. Standard mode reserves 1 + 2 = 3 lines.
+ * 3. Line budget = 10 - 3 = 7 lines.
+ * 4. 9 lines of output > 7 lines budget => hasOverflow should be TRUE.
+ */
+
+ const lines = Array.from({ length: 9 }, (_, i) => `line ${i + 1}`);
+ const resultDisplay = lines.join('\n');
+
+ const toolCalls: IndividualToolCallDisplay[] = [
+ {
+ callId: 'call-1',
+ name: 'test-tool',
+ description: 'a test tool',
+ status: CoreToolCallStatus.Success,
+ resultDisplay,
+ confirmationDetails: undefined,
+ },
+ ];
+
+ const { lastFrame } = renderWithProviders(
+
+
+ ,
+ {
+ uiState: {
+ streamingState: StreamingState.Idle,
+ constrainHeight: true,
+ },
+ useAlternateBuffer: false,
+ },
+ );
+
+ // Verify truncation is occurring (standard mode uses MaxSizedBox)
+ await waitFor(() => expect(lastFrame()).toContain('hidden ...'));
+
+ // In Standard mode, ToolGroupMessage calculates hasOverflow correctly now.
+ // While Standard mode doesn't render the inline hint (ShowMoreLines returns null),
+ // the logic inside ToolGroupMessage is now synchronized.
+ });
+});
diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx
index c24fb8e58b3..f7d158d68cf 100644
--- a/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.test.tsx
@@ -277,21 +277,47 @@ describe('ToolResultDisplay', () => {
inverse: false,
},
],
+ [
+ {
+ text: 'Line 4',
+ fg: '',
+ bg: '',
+ bold: false,
+ italic: false,
+ underline: false,
+ dim: false,
+ inverse: false,
+ },
+ ],
+ [
+ {
+ text: 'Line 5',
+ fg: '',
+ bg: '',
+ bold: false,
+ italic: false,
+ underline: false,
+ dim: false,
+ inverse: false,
+ },
+ ],
];
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
,
);
await waitUntilReady();
const output = lastFrame();
expect(output).not.toContain('Line 1');
- expect(output).toContain('Line 2');
- expect(output).toContain('Line 3');
+ expect(output).not.toContain('Line 2');
+ expect(output).not.toContain('Line 3');
+ expect(output).toContain('Line 4');
+ expect(output).toContain('Line 5');
unmount();
});
diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx
index 61f1540017e..8e0fc4442a5 100644
--- a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx
+++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx
@@ -19,10 +19,7 @@ import { Scrollable } from '../shared/Scrollable.js';
import { ScrollableList } from '../shared/ScrollableList.js';
import { SCROLL_TO_ITEM_END } from '../shared/VirtualizedList.js';
import { ACTIVE_SHELL_MAX_LINES } from '../../constants.js';
-
-const STATIC_HEIGHT = 1;
-const RESERVED_LINE_COUNT = 6; // for tool name, status, padding, and 'ShowMoreLines' hint
-const MIN_LINES_SHOWN = 2; // show at least this many lines
+import { calculateToolContentMaxLines } from '../../utils/toolLayoutUtils.js';
// Large threshold to ensure we don't cause performance issues for very large
// outputs that will get truncated further MaxSizedBox anyway.
@@ -53,16 +50,11 @@ export const ToolResultDisplay: React.FC = ({
const { renderMarkdown } = useUIState();
const isAlternateBuffer = useAlternateBuffer();
- let availableHeight = availableTerminalHeight
- ? Math.max(
- availableTerminalHeight - STATIC_HEIGHT - RESERVED_LINE_COUNT,
- MIN_LINES_SHOWN + 1, // enforce minimum lines shown
- )
- : undefined;
-
- if (maxLines && availableHeight) {
- availableHeight = Math.min(availableHeight, maxLines);
- }
+ const availableHeight = calculateToolContentMaxLines({
+ availableTerminalHeight,
+ isAlternateBuffer,
+ maxLinesLimit: maxLines,
+ });
const combinedPaddingAndBorderWidth = 4;
const childWidth = terminalWidth - combinedPaddingAndBorderWidth;
@@ -81,7 +73,8 @@ export const ToolResultDisplay: React.FC = ({
[],
);
- const truncatedResultDisplay = React.useMemo(() => {
+ const { truncatedResultDisplay, hiddenLinesCount } = React.useMemo(() => {
+ let hiddenLines = 0;
// Only truncate string output if not in alternate buffer mode to ensure
// we can scroll through the full output.
if (typeof resultDisplay === 'string' && !isAlternateBuffer) {
@@ -94,14 +87,29 @@ export const ToolResultDisplay: React.FC = ({
const contentText = hasTrailingNewline ? text.slice(0, -1) : text;
const lines = contentText.split('\n');
if (lines.length > maxLines) {
+ // We will have a label from MaxSizedBox. Reserve space for it.
+ const targetLines = Math.max(1, maxLines - 1);
+ hiddenLines = lines.length - targetLines;
text =
- lines.slice(-maxLines).join('\n') +
+ lines.slice(-targetLines).join('\n') +
(hasTrailingNewline ? '\n' : '');
}
}
- return text;
+ return { truncatedResultDisplay: text, hiddenLinesCount: hiddenLines };
+ }
+
+ if (Array.isArray(resultDisplay) && !isAlternateBuffer && maxLines) {
+ if (resultDisplay.length > maxLines) {
+ // We will have a label from MaxSizedBox. Reserve space for it.
+ const targetLines = Math.max(1, maxLines - 1);
+ return {
+ truncatedResultDisplay: resultDisplay.slice(-targetLines),
+ hiddenLinesCount: resultDisplay.length - targetLines,
+ };
+ }
}
- return resultDisplay;
+
+ return { truncatedResultDisplay: resultDisplay, hiddenLinesCount: 0 };
}, [resultDisplay, isAlternateBuffer, maxLines]);
if (!truncatedResultDisplay) return null;
@@ -229,7 +237,11 @@ export const ToolResultDisplay: React.FC = ({
return (
-
+
{content}
diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplayOverflow.test.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplayOverflow.test.tsx
index dd3184a19ca..a196b8d9897 100644
--- a/packages/cli/src/ui/components/messages/ToolResultDisplayOverflow.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolResultDisplayOverflow.test.tsx
@@ -39,6 +39,7 @@ describe('ToolResultDisplay Overflow', () => {
toolCalls={toolCalls}
availableTerminalHeight={15} // Small height to force overflow
terminalWidth={80}
+ isExpandable={true}
/>
,
{
@@ -46,26 +47,28 @@ describe('ToolResultDisplay Overflow', () => {
streamingState: StreamingState.Idle,
constrainHeight: true,
},
- useAlternateBuffer: false,
+ useAlternateBuffer: true,
},
);
// ResizeObserver might take a tick
await waitFor(() =>
- expect(lastFrame()).toContain('Press ctrl-o to show more lines'),
+ expect(lastFrame()?.toLowerCase()).toContain(
+ 'press ctrl+o to show more lines',
+ ),
);
const frame = lastFrame();
expect(frame).toBeDefined();
if (frame) {
- expect(frame).toContain('Press ctrl-o to show more lines');
+ expect(frame.toLowerCase()).toContain('press ctrl+o to show more lines');
// Ensure it's AFTER the bottom border
const linesOfOutput = frame.split('\n');
const bottomBorderIndex = linesOfOutput.findLastIndex((l) =>
l.includes('╰─'),
);
const hintIndex = linesOfOutput.findIndex((l) =>
- l.includes('Press ctrl-o to show more lines'),
+ l.toLowerCase().includes('press ctrl+o to show more lines'),
);
expect(hintIndex).toBeGreaterThan(bottomBorderIndex);
expect(frame).toMatchSnapshot();
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ShellToolMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ShellToolMessage.test.tsx.snap
index 7e3d34a5773..0d34c7e49d6 100644
--- a/packages/cli/src/ui/components/messages/__snapshots__/ShellToolMessage.test.tsx.snap
+++ b/packages/cli/src/ui/components/messages/__snapshots__/ShellToolMessage.test.tsx.snap
@@ -1,6 +1,6 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
-exports[` > Height Constraints > defaults to ACTIVE_SHELL_MAX_LINES when availableTerminalHeight is undefined 1`] = `
+exports[` > Height Constraints > defaults to ACTIVE_SHELL_MAX_LINES in alternate buffer when availableTerminalHeight is undefined 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ⊷ Shell Command A shell command │
│ │
@@ -22,6 +22,113 @@ exports[` > Height Constraints > defaults to ACTIVE_SHELL_MA
"
`;
+exports[` > Height Constraints > fully expands in alternate buffer mode when constrainHeight is false and isExpandable is true 1`] = `
+"╭──────────────────────────────────────────────────────────────────────────────╮
+│ ✓ Shell Command A shell command │
+│ │
+│ Line 1 │
+│ Line 2 │
+│ Line 3 │
+│ Line 4 │
+│ Line 5 │
+│ Line 6 │
+│ Line 7 │
+│ Line 8 │
+│ Line 9 │
+│ Line 10 │
+│ Line 11 │
+│ Line 12 │
+│ Line 13 │
+│ Line 14 │
+│ Line 15 │
+│ Line 16 │
+│ Line 17 │
+│ Line 18 │
+│ Line 19 │
+│ Line 20 │
+│ Line 21 │
+│ Line 22 │
+│ Line 23 │
+│ Line 24 │
+│ Line 25 │
+│ Line 26 │
+│ Line 27 │
+│ Line 28 │
+│ Line 29 │
+│ Line 30 │
+│ Line 31 │
+│ Line 32 │
+│ Line 33 │
+│ Line 34 │
+│ Line 35 │
+│ Line 36 │
+│ Line 37 │
+│ Line 38 │
+│ Line 39 │
+│ Line 40 │
+│ Line 41 │
+│ Line 42 │
+│ Line 43 │
+│ Line 44 │
+│ Line 45 │
+│ Line 46 │
+│ Line 47 │
+│ Line 48 │
+│ Line 49 │
+│ Line 50 │
+│ Line 51 │
+│ Line 52 │
+│ Line 53 │
+│ Line 54 │
+│ Line 55 │
+│ Line 56 │
+│ Line 57 │
+│ Line 58 │
+│ Line 59 │
+│ Line 60 │
+│ Line 61 │
+│ Line 62 │
+│ Line 63 │
+│ Line 64 │
+│ Line 65 │
+│ Line 66 │
+│ Line 67 │
+│ Line 68 │
+│ Line 69 │
+│ Line 70 │
+│ Line 71 │
+│ Line 72 │
+│ Line 73 │
+│ Line 74 │
+│ Line 75 │
+│ Line 76 │
+│ Line 77 │
+│ Line 78 │
+│ Line 79 │
+│ Line 80 │
+│ Line 81 │
+│ Line 82 │
+│ Line 83 │
+│ Line 84 │
+│ Line 85 │
+│ Line 86 │
+│ Line 87 │
+│ Line 88 │
+│ Line 89 │
+│ Line 90 │
+│ Line 91 │
+│ Line 92 │
+│ Line 93 │
+│ Line 94 │
+│ Line 95 │
+│ Line 96 │
+│ Line 97 │
+│ Line 98 │
+│ Line 99 │
+│ Line 100 │
+"
+`;
+
exports[` > Height Constraints > respects availableTerminalHeight when it is smaller than ACTIVE_SHELL_MAX_LINES 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ⊷ Shell Command A shell command │
@@ -37,6 +144,28 @@ exports[` > Height Constraints > respects availableTerminalH
"
`;
+exports[` > Height Constraints > stays constrained in alternate buffer mode when isExpandable is false even if constrainHeight is false 1`] = `
+"╭──────────────────────────────────────────────────────────────────────────────╮
+│ ✓ Shell Command A shell command │
+│ │
+│ Line 86 │
+│ Line 87 │
+│ Line 88 │
+│ Line 89 │
+│ Line 90 │
+│ Line 91 │
+│ Line 92 │
+│ Line 93 │
+│ Line 94 │
+│ Line 95 │
+│ Line 96 │
+│ Line 97 │
+│ Line 98 ▄ │
+│ Line 99 █ │
+│ Line 100 █ │
+"
+`;
+
exports[` > Height Constraints > uses ACTIVE_SHELL_MAX_LINES when availableTerminalHeight is large 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ⊷ Shell Command A shell command │
@@ -161,7 +290,6 @@ exports[` > Height Constraints > uses full availableTerminal
│ Line 98 █ │
│ Line 99 █ │
│ Line 100 █ │
-│ │
"
`;
@@ -170,7 +298,6 @@ exports[` > Snapshots > renders in Alternate Buffer mode whi
│ ⊷ Shell Command A shell command (Shift+Tab to unfocus) │
│ │
│ Test result │
-│ │
"
`;
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap
index c8a97070046..6adcb80a5cc 100644
--- a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap
+++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap
@@ -55,13 +55,13 @@ exports[` > Golden Snapshots > renders header when scrolled
"╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool-1 Description 1. This is a long description that will need to b… │
│──────────────────────────────────────────────────────────────────────────│
-│ │ ▄
+│ line5 │ █
+│ │ █
│ ✓ tool-2 Description 2 │ █
│ │ █
│ line1 │ █
│ line2 │ █
╰──────────────────────────────────────────────────────────────────────────╯ █
- █
"
`;
@@ -111,12 +111,12 @@ exports[` > Golden Snapshots > renders tool call with output
`;
exports[` > Golden Snapshots > renders two tool groups where only the last line of the previous group is visible 1`] = `
-"╭──────────────────────────────────────────────────────────────────────────╮
+"╰──────────────────────────────────────────────────────────────────────────╯
+╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool-2 Description 2 │
-│ │
-│ line1 │ ▄
+│ │ ▄
+│ line1 │ █
╰──────────────────────────────────────────────────────────────────────────╯ █
- █
"
`;
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap
index 1d9b58f0ce4..d1e4b16d2f0 100644
--- a/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap
+++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap
@@ -37,7 +37,11 @@ exports[`ToolResultDisplay > renders string result as plain text when renderOutp
`;
exports[`ToolResultDisplay > truncates very long string results 1`] = `
-"... first 252 lines hidden ...
+"... first 248 lines hidden ...
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplayOverflow.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplayOverflow.test.tsx.snap
index 3854b291db4..aab4b690a16 100644
--- a/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplayOverflow.test.tsx.snap
+++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplayOverflow.test.tsx.snap
@@ -4,13 +4,13 @@ exports[`ToolResultDisplay Overflow > should display "press ctrl-o" hint when co
"╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool a test tool │
│ │
-│ ... first 45 lines hidden ... │
+│ line 45 │
│ line 46 │
│ line 47 │
│ line 48 │
│ line 49 │
-│ line 50 │
+│ line 50 █ │
╰──────────────────────────────────────────────────────────────────────────╯
- Press ctrl-o to show more lines
+ Press Ctrl+O to show more lines
"
`;
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap
index 66ca527b4b5..dda93c1c21f 100644
--- a/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap
+++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolStickyHeaderRegression.test.tsx.snap
@@ -2,7 +2,7 @@
exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage in a ToolGroupMessage in a ScrollableList has sticky headers 1`] = `
"╭────────────────────────────────────────────────────────────────────────╮ █
-│ ✓ Shell Command Description for Shell Command │ ▀
+│ ✓ Shell Command Description for Shell Command │ █
│ │
│ shell-01 │
│ shell-02 │
@@ -11,7 +11,7 @@ exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage i
exports[`ToolMessage Sticky Header Regression > verifies that ShellToolMessage in a ToolGroupMessage in a ScrollableList has sticky headers 2`] = `
"╭────────────────────────────────────────────────────────────────────────╮
-│ ✓ Shell Command Description for Shell Command │
+│ ✓ Shell Command Description for Shell Command │ ▄
│────────────────────────────────────────────────────────────────────────│ █
│ shell-06 │ ▀
│ shell-07 │
diff --git a/packages/cli/src/ui/constants.ts b/packages/cli/src/ui/constants.ts
index 197711f82f2..93a3198ca8d 100644
--- a/packages/cli/src/ui/constants.ts
+++ b/packages/cli/src/ui/constants.ts
@@ -33,6 +33,7 @@ export const WARNING_PROMPT_DURATION_MS = 3000;
export const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000;
export const SHELL_ACTION_REQUIRED_TITLE_DELAY_MS = 30000;
export const SHELL_SILENT_WORKING_TITLE_DELAY_MS = 120000;
+export const EXPAND_HINT_DURATION_MS = 5000;
export const DEFAULT_BACKGROUND_OPACITY = 0.16;
export const DEFAULT_INPUT_BACKGROUND_OPACITY = 0.24;
diff --git a/packages/cli/src/ui/contexts/OverflowContext.tsx b/packages/cli/src/ui/contexts/OverflowContext.tsx
index 9d8b78d4c7f..cee02090b6a 100644
--- a/packages/cli/src/ui/contexts/OverflowContext.tsx
+++ b/packages/cli/src/ui/contexts/OverflowContext.tsx
@@ -13,13 +13,14 @@ import {
useMemo,
} from 'react';
-interface OverflowState {
+export interface OverflowState {
overflowingIds: ReadonlySet;
}
-interface OverflowActions {
+export interface OverflowActions {
addOverflowingId: (id: string) => void;
removeOverflowingId: (id: string) => void;
+ reset: () => void;
}
const OverflowStateContext = createContext(
@@ -63,6 +64,10 @@ export const OverflowProvider: React.FC<{ children: React.ReactNode }> = ({
});
}, []);
+ const reset = useCallback(() => {
+ setOverflowingIds(new Set());
+ }, []);
+
const stateValue = useMemo(
() => ({
overflowingIds,
@@ -74,8 +79,9 @@ export const OverflowProvider: React.FC<{ children: React.ReactNode }> = ({
() => ({
addOverflowingId,
removeOverflowingId,
+ reset,
}),
- [addOverflowingId, removeOverflowingId],
+ [addOverflowingId, removeOverflowingId, reset],
);
return (
diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx
index a1c63759e93..9fb2852361e 100644
--- a/packages/cli/src/ui/contexts/UIStateContext.tsx
+++ b/packages/cli/src/ui/contexts/UIStateContext.tsx
@@ -182,6 +182,7 @@ export interface UIState {
isBackgroundShellListOpen: boolean;
adminSettingsChanged: boolean;
newAgents: AgentDefinition[] | null;
+ showIsExpandableHint: boolean;
hintMode: boolean;
hintBuffer: string;
transientMessage: {
diff --git a/packages/cli/src/ui/utils/CodeColorizer.tsx b/packages/cli/src/ui/utils/CodeColorizer.tsx
index 1034e7372e4..56e34eefa4f 100644
--- a/packages/cli/src/ui/utils/CodeColorizer.tsx
+++ b/packages/cli/src/ui/utils/CodeColorizer.tsx
@@ -22,7 +22,6 @@ import {
} from '../components/shared/MaxSizedBox.js';
import type { LoadedSettings } from '../../config/settings.js';
import { debugLogger } from '@google/gemini-cli-core';
-import { isAlternateBufferEnabled } from '../hooks/useAlternateBuffer.js';
// Configure theming and parsing utilities.
const lowlight = createLowlight(common);
@@ -152,7 +151,6 @@ export function colorizeCode({
? false
: settings.merged.ui.showLineNumbers;
- const useMaxSizedBox = !isAlternateBufferEnabled(settings);
try {
// Render the HAST tree using the adapted theme
// Apply the theme's default foreground color to the top-level Text element
@@ -162,7 +160,7 @@ export function colorizeCode({
let hiddenLinesCount = 0;
// Optimization to avoid highlighting lines that cannot possibly be displayed.
- if (availableHeight !== undefined && useMaxSizedBox) {
+ if (availableHeight !== undefined) {
availableHeight = Math.max(availableHeight, MINIMUM_MAX_HEIGHT);
if (lines.length > availableHeight) {
const sliceIndex = lines.length - availableHeight;
@@ -200,7 +198,7 @@ export function colorizeCode({
);
});
- if (useMaxSizedBox) {
+ if (availableHeight !== undefined) {
return (
));
- if (useMaxSizedBox) {
+ if (availableHeight !== undefined) {
return (