Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -998,6 +998,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 @@ -1219,6 +1220,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 @@ -1487,6 +1495,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
constrainHeight,
showErrorDetails,
showFullTodos,
showFullOutput,
filteredConsoleMessages,
ideContextState,
renderMarkdown,
Expand Down Expand Up @@ -1575,6 +1584,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
Loading