diff --git a/packages/cli/src/config/keyBindings.ts b/packages/cli/src/config/keyBindings.ts index 497b359f2ef..c65a66e788a 100644 --- a/packages/cli/src/config/keyBindings.ts +++ b/packages/cli/src/config/keyBindings.ts @@ -59,6 +59,7 @@ export enum Command { // App level bindings SHOW_ERROR_DETAILS = 'showErrorDetails', SHOW_FULL_TODOS = 'showFullTodos', + SHOW_FULL_OUTPUT = 'showFullOutput', TOGGLE_IDE_CONTEXT_DETAIL = 'toggleIDEContextDetail', TOGGLE_MARKDOWN = 'toggleMarkdown', TOGGLE_COPY_MODE = 'toggleCopyMode', @@ -197,6 +198,7 @@ export const defaultKeyBindings: KeyBindingConfig = { // App level bindings [Command.SHOW_ERROR_DETAILS]: [{ key: 'f12' }], [Command.SHOW_FULL_TODOS]: [{ key: 't', ctrl: true }], + [Command.SHOW_FULL_OUTPUT]: [{ key: 'o', ctrl: true }], [Command.TOGGLE_IDE_CONTEXT_DETAIL]: [{ key: 'g', ctrl: true }], [Command.TOGGLE_MARKDOWN]: [{ key: 'm', command: true }], [Command.TOGGLE_COPY_MODE]: [{ key: 's', ctrl: true }], @@ -299,6 +301,7 @@ export const commandCategories: readonly CommandCategory[] = [ commands: [ Command.SHOW_ERROR_DETAILS, Command.SHOW_FULL_TODOS, + Command.SHOW_FULL_OUTPUT, Command.TOGGLE_IDE_CONTEXT_DETAIL, Command.TOGGLE_MARKDOWN, Command.TOGGLE_COPY_MODE, @@ -347,6 +350,8 @@ export const commandDescriptions: Readonly> = { [Command.PASTE_CLIPBOARD]: 'Paste from the clipboard.', [Command.SHOW_ERROR_DETAILS]: 'Toggle detailed error information.', [Command.SHOW_FULL_TODOS]: 'Toggle the full TODO list.', + [Command.SHOW_FULL_OUTPUT]: + 'Toggle display of full tool output when truncated.', [Command.TOGGLE_IDE_CONTEXT_DETAIL]: 'Toggle IDE context details.', [Command.TOGGLE_MARKDOWN]: 'Toggle Markdown rendering.', [Command.TOGGLE_COPY_MODE]: diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index 894fd065687..12c49efbaa4 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -133,6 +133,7 @@ const baseMockUiState = { mainAreaWidth: 100, terminalWidth: 120, currentModel: 'gemini-pro', + showFullOutput: false, }; const mockUIActions: UIActions = { diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 55ccc7438f5..c4ae3bb74d8 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1009,6 +1009,7 @@ Logging in with Google... Restarting Gemini CLI to continue. const [showErrorDetails, setShowErrorDetails] = useState(false); const [showFullTodos, setShowFullTodos] = useState(false); + const [showFullOutput, setShowFullOutput] = useState(false); const [renderMarkdown, setRenderMarkdown] = useState(true); const [ctrlCPressCount, setCtrlCPressCount] = useState(0); @@ -1230,6 +1231,13 @@ Logging in with Google... Restarting Gemini CLI to continue. setShowErrorDetails((prev) => !prev); } else if (keyMatchers[Command.SHOW_FULL_TODOS](key)) { setShowFullTodos((prev) => !prev); + } else if (keyMatchers[Command.SHOW_FULL_OUTPUT](key)) { + setShowFullOutput((prev) => { + const newValue = !prev; + // Force re-render of static content to show/hide full output + refreshStatic(); + return newValue; + }); } else if (keyMatchers[Command.TOGGLE_MARKDOWN](key)) { setRenderMarkdown((prev) => { const newValue = !prev; @@ -1498,6 +1506,7 @@ Logging in with Google... Restarting Gemini CLI to continue. constrainHeight, showErrorDetails, showFullTodos, + showFullOutput, filteredConsoleMessages, ideContextState, renderMarkdown, @@ -1586,6 +1595,7 @@ Logging in with Google... Restarting Gemini CLI to continue. constrainHeight, showErrorDetails, showFullTodos, + showFullOutput, filteredConsoleMessages, ideContextState, renderMarkdown, diff --git a/packages/cli/src/ui/components/AnsiOutput.tsx b/packages/cli/src/ui/components/AnsiOutput.tsx index d31ae62b28a..398ee828b97 100644 --- a/packages/cli/src/ui/components/AnsiOutput.tsx +++ b/packages/cli/src/ui/components/AnsiOutput.tsx @@ -8,8 +8,6 @@ import type React from 'react'; import { Box, Text } from 'ink'; import type { AnsiLine, AnsiOutput, AnsiToken } from '@google/gemini-cli-core'; -const DEFAULT_HEIGHT = 24; - interface AnsiOutputProps { data: AnsiOutput; availableTerminalHeight?: number; @@ -21,11 +19,11 @@ export const AnsiOutputText: React.FC = ({ availableTerminalHeight, width, }) => { - const lastLines = data.slice( - -(availableTerminalHeight && availableTerminalHeight > 0 - ? availableTerminalHeight - : DEFAULT_HEIGHT), - ); + // When availableTerminalHeight is undefined, show all lines + const lastLines = + availableTerminalHeight !== undefined && availableTerminalHeight > 0 + ? data.slice(-availableTerminalHeight) + : data; return ( {lastLines.map((line: AnsiLine, lineIndex: number) => ( diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx index d41ff534d02..8194f911ffb 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx @@ -5,8 +5,9 @@ */ import type React from 'react'; -import { useMemo } from 'react'; +import { useMemo, useState, useEffect } from 'react'; import { Box, Text } from 'ink'; +import * as fs from 'node:fs/promises'; import type { IndividualToolCallDisplay } from '../../types.js'; import { ToolCallStatus } from '../../types.js'; import { ToolMessage } from './ToolMessage.js'; @@ -16,6 +17,7 @@ import { theme } from '../../semantic-colors.js'; import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js'; import { SHELL_TOOL_NAME } from '@google/gemini-cli-core'; import { useConfig } from '../../contexts/ConfigContext.js'; +import { useUIState } from '../../contexts/UIStateContext.js'; interface ToolGroupMessageProps { groupId: number; @@ -37,6 +39,37 @@ export const ToolGroupMessage: React.FC = ({ activeShellPtyId, embeddedShellFocused, }) => { + const { showFullOutput } = useUIState(); + const [fullOutputContents, setFullOutputContents] = useState< + Record + >({}); + + // Load full output file contents when showFullOutput is toggled on + useEffect(() => { + if (!showFullOutput) { + setFullOutputContents({}); + return; + } + + const loadOutputFiles = async () => { + const contents: Record = {}; + for (const tool of toolCalls) { + if (tool.outputFile) { + try { + const content = await fs.readFile(tool.outputFile, 'utf-8'); + contents[tool.callId] = content; + } catch { + contents[tool.callId] = `[Error reading file: ${tool.outputFile}]`; + } + } + } + setFullOutputContents(contents); + }; + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + loadOutputFiles(); + }, [showFullOutput, toolCalls]); + const isEmbeddedShellFocused = embeddedShellFocused && toolCalls.some( @@ -167,10 +200,27 @@ export const ToolGroupMessage: React.FC = ({ /> )} {tool.outputFile && ( - - - Output too long and was saved to: {tool.outputFile} + + + Output saved to: {tool.outputFile} + {!showFullOutput && ' (Press Ctrl+O to view full output)'} + {showFullOutput && fullOutputContents[tool.callId] && ( + + + ─── Full Output ─── + + + {fullOutputContents[tool.callId]} + + + )} )} diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx index 49002d50e92..36f6ddde52a 100644 --- a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx +++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx @@ -41,15 +41,17 @@ export const ToolResultDisplay: React.FC = ({ terminalWidth, renderOutputAsMarkdown = true, }) => { - const { renderMarkdown } = useUIState(); + const { renderMarkdown, showFullOutput } = useUIState(); const isAlternateBuffer = useAlternateBuffer(); - const availableHeight = availableTerminalHeight - ? Math.max( - availableTerminalHeight - STATIC_HEIGHT - RESERVED_LINE_COUNT, - MIN_LINES_SHOWN + 1, // enforce minimum lines shown - ) - : undefined; + // When showFullOutput is true, don't limit the height + const availableHeight = + showFullOutput || !availableTerminalHeight + ? undefined + : Math.max( + availableTerminalHeight - STATIC_HEIGHT - RESERVED_LINE_COUNT, + MIN_LINES_SHOWN + 1, // enforce minimum lines shown + ); // Long tool call response in MarkdownDisplay doesn't respect availableTerminalHeight properly, // so if we aren't using alternate buffer mode, we're forcing it to not render as markdown when the response is too long, it will fallback diff --git a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx index 86b36cd28d4..0ff9821479f 100644 --- a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx +++ b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx @@ -188,14 +188,14 @@ export const MaxSizedBox: React.FC = ({ {totalHiddenLines > 0 && overflowDirection === 'top' && ( ... first {totalHiddenLines} line{totalHiddenLines === 1 ? '' : 's'}{' '} - hidden ... + hidden (Ctrl+O to show all) ... )} {visibleLines} {totalHiddenLines > 0 && overflowDirection === 'bottom' && ( ... last {totalHiddenLines} line{totalHiddenLines === 1 ? '' : 's'}{' '} - hidden ... + hidden (Ctrl+O to show all) ... )} diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index 34e6262f306..173ced9451c 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -128,6 +128,7 @@ export interface UIState { embeddedShellFocused: boolean; showDebugProfiler: boolean; showFullTodos: boolean; + showFullOutput: boolean; copyModeEnabled: boolean; warningMessage: string | null; bannerData: { diff --git a/packages/cli/src/ui/keyMatchers.test.ts b/packages/cli/src/ui/keyMatchers.test.ts index 0982e84b2ac..047950f02b3 100644 --- a/packages/cli/src/ui/keyMatchers.test.ts +++ b/packages/cli/src/ui/keyMatchers.test.ts @@ -79,6 +79,7 @@ describe('keyMatchers', () => { key.ctrl && key.name === 'f', [Command.EXPAND_SUGGESTION]: (key: Key) => key.name === 'right', [Command.COLLAPSE_SUGGESTION]: (key: Key) => key.name === 'left', + [Command.SHOW_FULL_OUTPUT]: (key: Key) => key.ctrl && key.name === 'o', }; // Test data for each command with positive and negative test cases @@ -336,6 +337,11 @@ describe('keyMatchers', () => { positive: [createKey('f', { ctrl: true })], negative: [createKey('f')], }, + { + command: Command.SHOW_FULL_OUTPUT, + positive: [createKey('o', { ctrl: true })], + negative: [createKey('o'), createKey('t', { ctrl: true })], + }, ]; describe('Data-driven key binding matches original logic', () => {