Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/cli/src/config/keyBindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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 }],
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -347,6 +350,8 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
[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]:
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/test-utils/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ const baseMockUiState = {
mainAreaWidth: 100,
terminalWidth: 120,
currentModel: 'gemini-pro',
showFullOutput: false,
};

const mockUIActions: UIActions = {
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,7 @@ Logging in with Google... Restarting Gemini CLI to continue.

const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
const [showFullTodos, setShowFullTodos] = useState<boolean>(false);
const [showFullOutput, setShowFullOutput] = useState<boolean>(false);
const [renderMarkdown, setRenderMarkdown] = useState<boolean>(true);

const [ctrlCPressCount, setCtrlCPressCount] = useState(0);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1498,6 +1506,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
constrainHeight,
showErrorDetails,
showFullTodos,
showFullOutput,
filteredConsoleMessages,
ideContextState,
renderMarkdown,
Expand Down Expand Up @@ -1586,6 +1595,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
constrainHeight,
showErrorDetails,
showFullTodos,
showFullOutput,
filteredConsoleMessages,
ideContextState,
renderMarkdown,
Expand Down
12 changes: 5 additions & 7 deletions packages/cli/src/ui/components/AnsiOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,11 +19,11 @@ export const AnsiOutputText: React.FC<AnsiOutputProps> = ({
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 (
<Box flexDirection="column" width={width} flexShrink={0}>
{lastLines.map((line: AnsiLine, lineIndex: number) => (
Expand Down
58 changes: 54 additions & 4 deletions packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -37,6 +39,37 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
activeShellPtyId,
embeddedShellFocused,
}) => {
const { showFullOutput } = useUIState();
const [fullOutputContents, setFullOutputContents] = useState<
Record<string, string>
>({});

// Load full output file contents when showFullOutput is toggled on
useEffect(() => {
if (!showFullOutput) {
setFullOutputContents({});
return;
}

const loadOutputFiles = async () => {
const contents: Record<string, string> = {};
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(
Expand Down Expand Up @@ -167,10 +200,27 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
/>
)}
{tool.outputFile && (
<Box>
<Text color={theme.text.primary}>
Output too long and was saved to: {tool.outputFile}
<Box flexDirection="column">
<Text color={theme.text.secondary}>
Output saved to: {tool.outputFile}
{!showFullOutput && ' (Press Ctrl+O to view full output)'}
</Text>
{showFullOutput && fullOutputContents[tool.callId] && (
<Box
flexDirection="column"
marginTop={1}
borderStyle="single"
borderColor={theme.border.default}
paddingX={1}
>
<Text color={theme.text.accent} bold>
─── Full Output ───
</Text>
<Text color={theme.text.primary} wrap="wrap">
{fullOutputContents[tool.callId]}
</Text>
</Box>
)}
</Box>
)}
</Box>
Expand Down
16 changes: 9 additions & 7 deletions packages/cli/src/ui/components/messages/ToolResultDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,17 @@ export const ToolResultDisplay: React.FC<ToolResultDisplayProps> = ({
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
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/ui/components/shared/MaxSizedBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,14 +188,14 @@ export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
{totalHiddenLines > 0 && overflowDirection === 'top' && (
<Text color={theme.text.secondary} wrap="truncate">
... first {totalHiddenLines} line{totalHiddenLines === 1 ? '' : 's'}{' '}
hidden ...
hidden (Ctrl+O to show all) ...
</Text>
)}
{visibleLines}
{totalHiddenLines > 0 && overflowDirection === 'bottom' && (
<Text color={theme.text.secondary} wrap="truncate">
... last {totalHiddenLines} line{totalHiddenLines === 1 ? '' : 's'}{' '}
hidden ...
hidden (Ctrl+O to show all) ...
</Text>
)}
</Box>
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/ui/contexts/UIStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export interface UIState {
embeddedShellFocused: boolean;
showDebugProfiler: boolean;
showFullTodos: boolean;
showFullOutput: boolean;
copyModeEnabled: boolean;
warningMessage: string | null;
bannerData: {
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/ui/keyMatchers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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', () => {
Expand Down