Skip to content

Commit 071e292

Browse files
authored
fix(ui): made shell tool header wrap on Ctrl+O (#26229)
1 parent 487fb21 commit 071e292

7 files changed

Lines changed: 136 additions & 7 deletions

File tree

packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,4 +356,65 @@ describe('<ShellToolMessage />', () => {
356356
unmount();
357357
});
358358
});
359+
360+
describe('Header Expansion', () => {
361+
const LONG_DESCRIPTION = 'very long '.repeat(20);
362+
363+
it('truncates header by default', async () => {
364+
const { lastFrame, waitUntilReady } = await renderWithProviders(
365+
<ShellToolMessage
366+
{...baseProps}
367+
description={LONG_DESCRIPTION}
368+
availableTerminalHeight={10}
369+
/>,
370+
{ uiActions },
371+
);
372+
373+
await waitUntilReady();
374+
const output = lastFrame();
375+
// Should be a single line header
376+
expect(output.split('\n')[1]).toContain(SHELL_COMMAND_NAME); // name
377+
// We check if it's truncated. In our ToolInfo, it's height 1.
378+
// The StickyHeader adds some structure, but the ToolInfo Box is inside.
379+
});
380+
381+
it('expands header when availableTerminalHeight is undefined', async () => {
382+
const { lastFrame, waitUntilReady } = await renderWithProviders(
383+
<ShellToolMessage
384+
{...baseProps}
385+
description={LONG_DESCRIPTION}
386+
availableTerminalHeight={undefined}
387+
/>,
388+
{ uiActions },
389+
);
390+
391+
await waitUntilReady();
392+
const output = lastFrame();
393+
// When expanded, the header (ToolInfo) should wrap and take multiple lines.
394+
// Since it's at the top, we check if the first few lines contain parts of the description.
395+
const lines = output.split('\n');
396+
expect(lines.length).toBeGreaterThan(5);
397+
});
398+
399+
it('expands header when isExpanded is true in context', async () => {
400+
const { lastFrame, waitUntilReady } = await renderWithProviders(
401+
<ShellToolMessage
402+
{...baseProps}
403+
description={LONG_DESCRIPTION}
404+
availableTerminalHeight={10}
405+
/>,
406+
{
407+
uiActions,
408+
toolActions: {
409+
isExpanded: (id: string) => id === baseProps.callId,
410+
},
411+
},
412+
);
413+
414+
await waitUntilReady();
415+
const output = lastFrame();
416+
// Should be expanded due to context
417+
expect(output.split('\n').length).toBeGreaterThan(5);
418+
});
419+
});
359420
});

packages/cli/src/ui/components/messages/ShellToolMessage.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type { ToolMessageProps } from './ToolMessage.js';
2424
import { ACTIVE_SHELL_MAX_LINES } from '../../constants.js';
2525
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
2626
import { useUIState } from '../../contexts/UIStateContext.js';
27+
import { useToolActions } from '../../contexts/ToolActionsContext.js';
2728
import {
2829
type Config,
2930
ShellExecutionService,
@@ -41,6 +42,7 @@ export interface ShellToolMessageProps extends ToolMessageProps {
4142
}
4243

4344
export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
45+
callId,
4446
name,
4547
description,
4648
resultDisplay,
@@ -57,6 +59,12 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
5759
isExpandable,
5860
originalRequestName,
5961
}) => {
62+
const { isExpanded: isExpandedInContext } = useToolActions();
63+
64+
const isExpanded =
65+
(isExpandedInContext ? isExpandedInContext(callId) : false) ||
66+
availableTerminalHeight === undefined;
67+
6068
const {
6169
activePtyId: activeShellPtyId,
6270
embeddedShellFocused,
@@ -169,6 +177,7 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
169177
description={description}
170178
emphasis={emphasis}
171179
originalRequestName={originalRequestName}
180+
isExpanded={isExpanded}
172181
/>
173182

174183
<FocusHint

packages/cli/src/ui/components/messages/ToolMessage.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
import { type Config, CoreToolCallStatus, Kind } from '@google/gemini-cli-core';
2525
import { ShellInputPrompt } from '../ShellInputPrompt.js';
2626
import { SUBAGENT_MAX_LINES } from '../../constants.js';
27+
import { useToolActions } from '../../contexts/ToolActionsContext.js';
2728

2829
export type { TextEmphasis };
2930

@@ -42,6 +43,7 @@ export interface ToolMessageProps extends IndividualToolCallDisplay {
4243
}
4344

4445
export const ToolMessage: React.FC<ToolMessageProps> = ({
46+
callId,
4547
name,
4648
description,
4749
resultDisplay,
@@ -63,6 +65,12 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
6365
progress,
6466
progressTotal,
6567
}) => {
68+
const { isExpanded: isExpandedInContext } = useToolActions();
69+
70+
const isExpanded =
71+
(isExpandedInContext ? isExpandedInContext(callId) : false) ||
72+
availableTerminalHeight === undefined;
73+
6674
const isThisShellFocused = checkIsShellFocused(
6775
name,
6876
status,
@@ -102,6 +110,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
102110
emphasis={emphasis}
103111
progressMessage={progressMessage}
104112
originalRequestName={originalRequestName}
113+
isExpanded={isExpanded}
105114
/>
106115
<FocusHint
107116
shouldShowFocusHint={shouldShowFocusHint}

packages/cli/src/ui/components/messages/ToolShared.test.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import { describe, it, expect, vi } from 'vitest';
88
import { render } from '../../../test-utils/render.js';
99
import { Text } from 'ink';
10-
import { McpProgressIndicator } from './ToolShared.js';
10+
import { McpProgressIndicator, ToolInfo } from './ToolShared.js';
11+
import { CoreToolCallStatus } from '@google/gemini-cli-core';
1112

1213
vi.mock('../GeminiRespondingSpinner.js', () => ({
1314
GeminiRespondingSpinner: () => <Text>MockSpinner</Text>,
@@ -65,3 +66,37 @@ describe('McpProgressIndicator', () => {
6566
expect(output).not.toContain('150%');
6667
});
6768
});
69+
70+
describe('ToolInfo', () => {
71+
const longDescription = 'long '.repeat(50);
72+
73+
it('truncates description by default', async () => {
74+
const { lastFrame } = await render(
75+
<ToolInfo
76+
name="test-tool"
77+
description={longDescription}
78+
status={CoreToolCallStatus.Success}
79+
emphasis="medium"
80+
/>,
81+
);
82+
const output = lastFrame();
83+
// In Ink, a single line Box with wrap="truncate" will be truncated.
84+
// Since we don't know the exact terminal width in this test, we check if it is short.
85+
expect(output.trim().split('\n').length).toBe(1);
86+
});
87+
88+
it('wraps description when isExpanded is true', async () => {
89+
const { lastFrame } = await render(
90+
<ToolInfo
91+
name="test-tool"
92+
description={longDescription}
93+
status={CoreToolCallStatus.Success}
94+
emphasis="medium"
95+
isExpanded={true}
96+
/>,
97+
);
98+
const output = lastFrame();
99+
// When expanded, it should wrap into multiple lines.
100+
expect(output.trim().split('\n').length).toBeGreaterThan(1);
101+
});
102+
});

packages/cli/src/ui/components/messages/ToolShared.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ type ToolInfoProps = {
194194
emphasis: TextEmphasis;
195195
progressMessage?: string;
196196
originalRequestName?: string;
197+
isExpanded?: boolean;
197198
};
198199

199200
export const ToolInfo: React.FC<ToolInfoProps> = ({
@@ -203,6 +204,7 @@ export const ToolInfo: React.FC<ToolInfoProps> = ({
203204
emphasis,
204205
progressMessage: _progressMessage,
205206
originalRequestName,
207+
isExpanded = false,
206208
}) => {
207209
const status = mapCoreStatusToDisplayStatus(coreStatus);
208210
const nameColor = React.useMemo<string>(() => {
@@ -224,8 +226,16 @@ export const ToolInfo: React.FC<ToolInfoProps> = ({
224226
const isCompletedAskUser = isCompletedAskUserTool(name, status);
225227

226228
return (
227-
<Box overflow="hidden" height={1} flexGrow={1} flexShrink={1}>
228-
<Text strikethrough={status === ToolCallStatus.Canceled} wrap="truncate">
229+
<Box
230+
overflow="hidden"
231+
height={isExpanded ? undefined : 1}
232+
flexGrow={1}
233+
flexShrink={1}
234+
>
235+
<Text
236+
strikethrough={status === ToolCallStatus.Canceled}
237+
wrap={isExpanded ? 'wrap' : 'truncate'}
238+
>
229239
<Text color={nameColor} bold>
230240
{name}
231241
</Text>

packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders empty tool calls arra
6262

6363
exports[`<ToolGroupMessage /> > Golden Snapshots > renders header when scrolled 1`] = `
6464
"╭──────────────────────────────────────────────────────────────────────────╮
65-
│ ✓ tool-1 Description 1. This is a long description that will need to b… │ ▄
65+
│ ✓ tool-1 Description 1. This is a long description that will need to be │
66+
│ truncated if the terminal width is small. │ ▄
6667
│──────────────────────────────────────────────────────────────────────────│ █
67-
│ line3 │ █
6868
│ line4 │ █
6969
│ line5 │ █
7070
│ │ █
@@ -161,7 +161,10 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders with limited terminal
161161

162162
exports[`<ToolGroupMessage /> > Golden Snapshots > renders with narrow terminal width 1`] = `
163163
"╭──────────────────────────────────╮
164-
│ ✓ very-long-tool-name-that-mig… │
164+
│ ✓ very-long-tool-name-that-migh │
165+
│ t-wrap This is a very long │
166+
│ description that might cause │
167+
│ wrapping issues │
165168
│ │
166169
│ Test result │
167170
╰──────────────────────────────────╯

packages/cli/src/ui/components/messages/__snapshots__/ToolMessageFocusHint.test.tsx.snap

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@ exports[`Focus Hint > 'ToolMessage' > shows focus hint after delay with output >
5858

5959
exports[`Focus Hint > handles long descriptions by shrinking them to show the focus hint > long-description 1`] = `
6060
"╭──────────────────────────────────────────────────────────────────────────────╮
61-
│ ⊶ Shell Command AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA… (Tab to focus) │
61+
│ ⊶ Shell Command (Tab to focus) │
62+
│ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA │
63+
│ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA │
6264
│ │
6365
"
6466
`;

0 commit comments

Comments
 (0)