Skip to content

Commit a17691f

Browse files
authored
feat(core): improve subagent result display (google-gemini#20378)
1 parent d246315 commit a17691f

21 files changed

Lines changed: 919 additions & 232 deletions

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

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -191,49 +191,63 @@ describe('<ShellToolMessage />', () => {
191191
10,
192192
8,
193193
false,
194+
true,
194195
],
195196
[
196197
'uses ACTIVE_SHELL_MAX_LINES when availableTerminalHeight is large',
197198
100,
198199
ACTIVE_SHELL_MAX_LINES - 3,
199200
false,
201+
true,
200202
],
201203
[
202204
'uses full availableTerminalHeight when focused in alternate buffer mode',
203205
100,
204206
98, // 100 - 2
205207
true,
208+
false,
206209
],
207210
[
208211
'defaults to ACTIVE_SHELL_MAX_LINES in alternate buffer when availableTerminalHeight is undefined',
209212
undefined,
210213
ACTIVE_SHELL_MAX_LINES - 3,
211214
false,
215+
false,
212216
],
213-
])('%s', async (_, availableTerminalHeight, expectedMaxLines, focused) => {
214-
const { lastFrame, waitUntilReady, unmount } = renderShell(
215-
{
216-
resultDisplay: LONG_OUTPUT,
217-
renderOutputAsMarkdown: false,
218-
availableTerminalHeight,
219-
ptyId: 1,
220-
status: CoreToolCallStatus.Executing,
221-
},
222-
{
223-
useAlternateBuffer: true,
224-
uiState: {
225-
activePtyId: focused ? 1 : 2,
226-
embeddedShellFocused: focused,
217+
])(
218+
'%s',
219+
async (
220+
_,
221+
availableTerminalHeight,
222+
expectedMaxLines,
223+
focused,
224+
constrainHeight,
225+
) => {
226+
const { lastFrame, waitUntilReady, unmount } = renderShell(
227+
{
228+
resultDisplay: LONG_OUTPUT,
229+
renderOutputAsMarkdown: false,
230+
availableTerminalHeight,
231+
ptyId: 1,
232+
status: CoreToolCallStatus.Executing,
227233
},
228-
},
229-
);
234+
{
235+
useAlternateBuffer: true,
236+
uiState: {
237+
activePtyId: focused ? 1 : 2,
238+
embeddedShellFocused: focused,
239+
constrainHeight,
240+
},
241+
},
242+
);
230243

231-
await waitUntilReady();
232-
const frame = lastFrame();
233-
expect(frame.match(/Line \d+/g)?.length).toBe(expectedMaxLines);
234-
expect(frame).toMatchSnapshot();
235-
unmount();
236-
});
244+
await waitUntilReady();
245+
const frame = lastFrame();
246+
expect(frame.match(/Line \d+/g)?.length).toBe(expectedMaxLines);
247+
expect(frame).toMatchSnapshot();
248+
unmount();
249+
},
250+
);
237251

238252
it('fully expands in standard mode when availableTerminalHeight is undefined', async () => {
239253
const { lastFrame, unmount } = renderShell(

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

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import { ToolMessage, type ToolMessageProps } from './ToolMessage.js';
99
import { describe, it, expect, vi } from 'vitest';
1010
import { StreamingState } from '../../types.js';
1111
import { Text } from 'ink';
12-
import { type AnsiOutput, CoreToolCallStatus } from '@google/gemini-cli-core';
12+
import {
13+
type AnsiOutput,
14+
CoreToolCallStatus,
15+
Kind,
16+
} from '@google/gemini-cli-core';
1317
import { renderWithProviders } from '../../../test-utils/render.js';
1418
import { tryParseJSON } from '../../../utils/jsonoutput.js';
1519

@@ -435,4 +439,99 @@ describe('<ToolMessage />', () => {
435439
expect(output).toMatchSnapshot();
436440
unmount();
437441
});
442+
443+
describe('Truncation', () => {
444+
it('applies truncation for Kind.Agent when availableTerminalHeight is provided', async () => {
445+
const multilineString = Array.from(
446+
{ length: 30 },
447+
(_, i) => `Line ${i + 1}`,
448+
).join('\n');
449+
450+
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
451+
<ToolMessage
452+
{...baseProps}
453+
kind={Kind.Agent}
454+
resultDisplay={multilineString}
455+
renderOutputAsMarkdown={false}
456+
availableTerminalHeight={40}
457+
/>,
458+
{
459+
uiActions,
460+
uiState: {
461+
streamingState: StreamingState.Idle,
462+
constrainHeight: true,
463+
},
464+
width: 80,
465+
useAlternateBuffer: false,
466+
},
467+
);
468+
await waitUntilReady();
469+
const output = lastFrame();
470+
471+
// Since kind=Kind.Agent and availableTerminalHeight is provided, it should truncate to SUBAGENT_MAX_LINES (15)
472+
// and show the FIRST lines (overflowDirection='bottom')
473+
expect(output).toContain('Line 1');
474+
expect(output).toContain('Line 14');
475+
expect(output).not.toContain('Line 16');
476+
expect(output).not.toContain('Line 30');
477+
unmount();
478+
});
479+
480+
it('does NOT apply truncation for Kind.Agent when availableTerminalHeight is undefined', async () => {
481+
const multilineString = Array.from(
482+
{ length: 30 },
483+
(_, i) => `Line ${i + 1}`,
484+
).join('\n');
485+
486+
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
487+
<ToolMessage
488+
{...baseProps}
489+
kind={Kind.Agent}
490+
resultDisplay={multilineString}
491+
renderOutputAsMarkdown={false}
492+
availableTerminalHeight={undefined}
493+
/>,
494+
{
495+
uiActions,
496+
uiState: { streamingState: StreamingState.Idle },
497+
width: 80,
498+
useAlternateBuffer: false,
499+
},
500+
);
501+
await waitUntilReady();
502+
const output = lastFrame();
503+
504+
expect(output).toContain('Line 1');
505+
expect(output).toContain('Line 30');
506+
unmount();
507+
});
508+
509+
it('does NOT apply truncation for Kind.Read', async () => {
510+
const multilineString = Array.from(
511+
{ length: 30 },
512+
(_, i) => `Line ${i + 1}`,
513+
).join('\n');
514+
515+
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
516+
<ToolMessage
517+
{...baseProps}
518+
kind={Kind.Read}
519+
resultDisplay={multilineString}
520+
renderOutputAsMarkdown={false}
521+
/>,
522+
{
523+
uiActions,
524+
uiState: { streamingState: StreamingState.Idle },
525+
width: 80,
526+
useAlternateBuffer: false,
527+
},
528+
);
529+
await waitUntilReady();
530+
const output = lastFrame();
531+
532+
expect(output).toContain('Line 1');
533+
expect(output).toContain('Line 30');
534+
unmount();
535+
});
536+
});
438537
});

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ import {
2121
useFocusHint,
2222
FocusHint,
2323
} from './ToolShared.js';
24-
import { type Config, CoreToolCallStatus } from '@google/gemini-cli-core';
24+
import { type Config, CoreToolCallStatus, Kind } from '@google/gemini-cli-core';
2525
import { ShellInputPrompt } from '../ShellInputPrompt.js';
26+
import { SUBAGENT_MAX_LINES } from '../../constants.js';
2627

2728
export type { TextEmphasis };
2829

@@ -45,6 +46,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
4546
description,
4647
resultDisplay,
4748
status,
49+
kind,
4850
availableTerminalHeight,
4951
terminalWidth,
5052
emphasis = 'medium',
@@ -133,6 +135,12 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
133135
terminalWidth={terminalWidth}
134136
renderOutputAsMarkdown={renderOutputAsMarkdown}
135137
hasFocus={isThisShellFocused}
138+
maxLines={
139+
kind === Kind.Agent && availableTerminalHeight !== undefined
140+
? SUBAGENT_MAX_LINES
141+
: undefined
142+
}
143+
overflowDirection={kind === Kind.Agent ? 'bottom' : 'top'}
136144
/>
137145
{isThisShellFocused && config && (
138146
<Box paddingLeft={STATUS_INDICATOR_WIDTH} marginTop={1}>

0 commit comments

Comments
 (0)