mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-30 23:14:32 -07:00
fix(ui): made shell tool header wrap on Ctrl+O (#26229)
This commit is contained in:
@@ -356,4 +356,65 @@ describe('<ShellToolMessage />', () => {
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Header Expansion', () => {
|
||||
const LONG_DESCRIPTION = 'very long '.repeat(20);
|
||||
|
||||
it('truncates header by default', async () => {
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
<ShellToolMessage
|
||||
{...baseProps}
|
||||
description={LONG_DESCRIPTION}
|
||||
availableTerminalHeight={10}
|
||||
/>,
|
||||
{ uiActions },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
// Should be a single line header
|
||||
expect(output.split('\n')[1]).toContain(SHELL_COMMAND_NAME); // name
|
||||
// We check if it's truncated. In our ToolInfo, it's height 1.
|
||||
// The StickyHeader adds some structure, but the ToolInfo Box is inside.
|
||||
});
|
||||
|
||||
it('expands header when availableTerminalHeight is undefined', async () => {
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
<ShellToolMessage
|
||||
{...baseProps}
|
||||
description={LONG_DESCRIPTION}
|
||||
availableTerminalHeight={undefined}
|
||||
/>,
|
||||
{ uiActions },
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
// When expanded, the header (ToolInfo) should wrap and take multiple lines.
|
||||
// Since it's at the top, we check if the first few lines contain parts of the description.
|
||||
const lines = output.split('\n');
|
||||
expect(lines.length).toBeGreaterThan(5);
|
||||
});
|
||||
|
||||
it('expands header when isExpanded is true in context', async () => {
|
||||
const { lastFrame, waitUntilReady } = await renderWithProviders(
|
||||
<ShellToolMessage
|
||||
{...baseProps}
|
||||
description={LONG_DESCRIPTION}
|
||||
availableTerminalHeight={10}
|
||||
/>,
|
||||
{
|
||||
uiActions,
|
||||
toolActions: {
|
||||
isExpanded: (id: string) => id === baseProps.callId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await waitUntilReady();
|
||||
const output = lastFrame();
|
||||
// Should be expanded due to context
|
||||
expect(output.split('\n').length).toBeGreaterThan(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import type { ToolMessageProps } from './ToolMessage.js';
|
||||
import { ACTIVE_SHELL_MAX_LINES } from '../../constants.js';
|
||||
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
|
||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { useToolActions } from '../../contexts/ToolActionsContext.js';
|
||||
import {
|
||||
type Config,
|
||||
ShellExecutionService,
|
||||
@@ -41,6 +42,7 @@ export interface ShellToolMessageProps extends ToolMessageProps {
|
||||
}
|
||||
|
||||
export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
||||
callId,
|
||||
name,
|
||||
description,
|
||||
resultDisplay,
|
||||
@@ -57,6 +59,12 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
||||
isExpandable,
|
||||
originalRequestName,
|
||||
}) => {
|
||||
const { isExpanded: isExpandedInContext } = useToolActions();
|
||||
|
||||
const isExpanded =
|
||||
(isExpandedInContext ? isExpandedInContext(callId) : false) ||
|
||||
availableTerminalHeight === undefined;
|
||||
|
||||
const {
|
||||
activePtyId: activeShellPtyId,
|
||||
embeddedShellFocused,
|
||||
@@ -169,6 +177,7 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
|
||||
description={description}
|
||||
emphasis={emphasis}
|
||||
originalRequestName={originalRequestName}
|
||||
isExpanded={isExpanded}
|
||||
/>
|
||||
|
||||
<FocusHint
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { type Config, CoreToolCallStatus, Kind } from '@google/gemini-cli-core';
|
||||
import { ShellInputPrompt } from '../ShellInputPrompt.js';
|
||||
import { SUBAGENT_MAX_LINES } from '../../constants.js';
|
||||
import { useToolActions } from '../../contexts/ToolActionsContext.js';
|
||||
|
||||
export type { TextEmphasis };
|
||||
|
||||
@@ -42,6 +43,7 @@ export interface ToolMessageProps extends IndividualToolCallDisplay {
|
||||
}
|
||||
|
||||
export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
callId,
|
||||
name,
|
||||
description,
|
||||
resultDisplay,
|
||||
@@ -63,6 +65,12 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
progress,
|
||||
progressTotal,
|
||||
}) => {
|
||||
const { isExpanded: isExpandedInContext } = useToolActions();
|
||||
|
||||
const isExpanded =
|
||||
(isExpandedInContext ? isExpandedInContext(callId) : false) ||
|
||||
availableTerminalHeight === undefined;
|
||||
|
||||
const isThisShellFocused = checkIsShellFocused(
|
||||
name,
|
||||
status,
|
||||
@@ -102,6 +110,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
emphasis={emphasis}
|
||||
progressMessage={progressMessage}
|
||||
originalRequestName={originalRequestName}
|
||||
isExpanded={isExpanded}
|
||||
/>
|
||||
<FocusHint
|
||||
shouldShowFocusHint={shouldShowFocusHint}
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render } from '../../../test-utils/render.js';
|
||||
import { Text } from 'ink';
|
||||
import { McpProgressIndicator } from './ToolShared.js';
|
||||
import { McpProgressIndicator, ToolInfo } from './ToolShared.js';
|
||||
import { CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('../GeminiRespondingSpinner.js', () => ({
|
||||
GeminiRespondingSpinner: () => <Text>MockSpinner</Text>,
|
||||
@@ -65,3 +66,37 @@ describe('McpProgressIndicator', () => {
|
||||
expect(output).not.toContain('150%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ToolInfo', () => {
|
||||
const longDescription = 'long '.repeat(50);
|
||||
|
||||
it('truncates description by default', async () => {
|
||||
const { lastFrame } = await render(
|
||||
<ToolInfo
|
||||
name="test-tool"
|
||||
description={longDescription}
|
||||
status={CoreToolCallStatus.Success}
|
||||
emphasis="medium"
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
// In Ink, a single line Box with wrap="truncate" will be truncated.
|
||||
// Since we don't know the exact terminal width in this test, we check if it is short.
|
||||
expect(output.trim().split('\n').length).toBe(1);
|
||||
});
|
||||
|
||||
it('wraps description when isExpanded is true', async () => {
|
||||
const { lastFrame } = await render(
|
||||
<ToolInfo
|
||||
name="test-tool"
|
||||
description={longDescription}
|
||||
status={CoreToolCallStatus.Success}
|
||||
emphasis="medium"
|
||||
isExpanded={true}
|
||||
/>,
|
||||
);
|
||||
const output = lastFrame();
|
||||
// When expanded, it should wrap into multiple lines.
|
||||
expect(output.trim().split('\n').length).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -194,6 +194,7 @@ type ToolInfoProps = {
|
||||
emphasis: TextEmphasis;
|
||||
progressMessage?: string;
|
||||
originalRequestName?: string;
|
||||
isExpanded?: boolean;
|
||||
};
|
||||
|
||||
export const ToolInfo: React.FC<ToolInfoProps> = ({
|
||||
@@ -203,6 +204,7 @@ export const ToolInfo: React.FC<ToolInfoProps> = ({
|
||||
emphasis,
|
||||
progressMessage: _progressMessage,
|
||||
originalRequestName,
|
||||
isExpanded = false,
|
||||
}) => {
|
||||
const status = mapCoreStatusToDisplayStatus(coreStatus);
|
||||
const nameColor = React.useMemo<string>(() => {
|
||||
@@ -224,8 +226,16 @@ export const ToolInfo: React.FC<ToolInfoProps> = ({
|
||||
const isCompletedAskUser = isCompletedAskUserTool(name, status);
|
||||
|
||||
return (
|
||||
<Box overflow="hidden" height={1} flexGrow={1} flexShrink={1}>
|
||||
<Text strikethrough={status === ToolCallStatus.Canceled} wrap="truncate">
|
||||
<Box
|
||||
overflow="hidden"
|
||||
height={isExpanded ? undefined : 1}
|
||||
flexGrow={1}
|
||||
flexShrink={1}
|
||||
>
|
||||
<Text
|
||||
strikethrough={status === ToolCallStatus.Canceled}
|
||||
wrap={isExpanded ? 'wrap' : 'truncate'}
|
||||
>
|
||||
<Text color={nameColor} bold>
|
||||
{name}
|
||||
</Text>
|
||||
|
||||
+6
-3
@@ -62,9 +62,9 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders empty tool calls arra
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders header when scrolled 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool-1 Description 1. This is a long description that will need to b… │ ▄
|
||||
│ ✓ tool-1 Description 1. This is a long description that will need to be │
|
||||
│ truncated if the terminal width is small. │ ▄
|
||||
│──────────────────────────────────────────────────────────────────────────│ █
|
||||
│ line3 │ █
|
||||
│ line4 │ █
|
||||
│ line5 │ █
|
||||
│ │ █
|
||||
@@ -161,7 +161,10 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders with limited terminal
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with narrow terminal width 1`] = `
|
||||
"╭──────────────────────────────────╮
|
||||
│ ✓ very-long-tool-name-that-mig… │
|
||||
│ ✓ very-long-tool-name-that-migh │
|
||||
│ t-wrap This is a very long │
|
||||
│ description that might cause │
|
||||
│ wrapping issues │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────╯
|
||||
|
||||
+3
-1
@@ -58,7 +58,9 @@ exports[`Focus Hint > 'ToolMessage' > shows focus hint after delay with output >
|
||||
|
||||
exports[`Focus Hint > handles long descriptions by shrinking them to show the focus hint > long-description 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ⊶ Shell Command AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA… (Tab to focus) │
|
||||
│ ⊶ Shell Command (Tab to focus) │
|
||||
│ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA │
|
||||
│ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA │
|
||||
│ │
|
||||
"
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user