Add low/full CLI error verbosity mode for cleaner UI (#20399)

This commit is contained in:
Dmitry Lyalin
2026-02-27 14:15:10 -05:00
committed by GitHub
parent 1c8951334a
commit 7f8ce8657c
25 changed files with 689 additions and 32 deletions

View File

@@ -25,6 +25,7 @@ import {
GLOB_DISPLAY_NAME,
} from '@google/gemini-cli-core';
import os from 'node:os';
import { createMockSettings } from '../../../test-utils/settings.js';
describe('<ToolGroupMessage />', () => {
afterEach(() => {
@@ -64,6 +65,11 @@ describe('<ToolGroupMessage />', () => {
ideMode: false,
enableInteractiveShell: true,
});
const fullVerbositySettings = createMockSettings({
merged: {
ui: { errorVerbosity: 'full' },
},
});
describe('Golden Snapshots', () => {
it('renders single successful tool call', async () => {
@@ -73,6 +79,7 @@ describe('<ToolGroupMessage />', () => {
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{
config: baseMockConfig,
settings: fullVerbositySettings,
uiState: {
pendingHistoryItems: [
{
@@ -104,7 +111,7 @@ describe('<ToolGroupMessage />', () => {
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{ config: baseMockConfig },
{ config: baseMockConfig, settings: fullVerbositySettings },
);
// Should render nothing because all tools in the group are confirming
@@ -140,6 +147,7 @@ describe('<ToolGroupMessage />', () => {
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{
config: baseMockConfig,
settings: fullVerbositySettings,
uiState: {
pendingHistoryItems: [
{
@@ -160,6 +168,76 @@ describe('<ToolGroupMessage />', () => {
unmount();
});
it('hides errored tool calls in low error verbosity mode', async () => {
const toolCalls = [
createToolCall({
callId: 'tool-1',
name: 'successful-tool',
status: CoreToolCallStatus.Success,
}),
createToolCall({
callId: 'tool-2',
name: 'error-tool',
status: CoreToolCallStatus.Error,
resultDisplay: 'Tool failed',
}),
];
const item = createItem(toolCalls);
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{
config: baseMockConfig,
uiState: {
pendingHistoryItems: [
{
type: 'tool_group',
tools: toolCalls,
},
],
},
},
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('successful-tool');
expect(output).not.toContain('error-tool');
unmount();
});
it('keeps client-initiated errored tool calls visible in low error verbosity mode', async () => {
const toolCalls = [
createToolCall({
callId: 'tool-1',
name: 'client-error-tool',
status: CoreToolCallStatus.Error,
isClientInitiated: true,
resultDisplay: 'Client tool failed',
}),
];
const item = createItem(toolCalls);
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{
config: baseMockConfig,
uiState: {
pendingHistoryItems: [
{
type: 'tool_group',
tools: toolCalls,
},
],
},
},
);
await waitUntilReady();
const output = lastFrame();
expect(output).toContain('client-error-tool');
unmount();
});
it('renders mixed tool calls including shell command', async () => {
const toolCalls = [
createToolCall({
@@ -187,6 +265,7 @@ describe('<ToolGroupMessage />', () => {
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{
config: baseMockConfig,
settings: fullVerbositySettings,
uiState: {
pendingHistoryItems: [
{
@@ -233,6 +312,7 @@ describe('<ToolGroupMessage />', () => {
/>,
{
config: baseMockConfig,
settings: fullVerbositySettings,
uiState: {
pendingHistoryItems: [
{
@@ -266,6 +346,7 @@ describe('<ToolGroupMessage />', () => {
/>,
{
config: baseMockConfig,
settings: fullVerbositySettings,
uiState: {
pendingHistoryItems: [
{
@@ -288,6 +369,7 @@ describe('<ToolGroupMessage />', () => {
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{
config: baseMockConfig,
settings: fullVerbositySettings,
uiState: {
pendingHistoryItems: [
{
@@ -326,6 +408,7 @@ describe('<ToolGroupMessage />', () => {
</Scrollable>,
{
config: baseMockConfig,
settings: fullVerbositySettings,
uiState: {
pendingHistoryItems: [
{
@@ -356,6 +439,7 @@ describe('<ToolGroupMessage />', () => {
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{
config: baseMockConfig,
settings: fullVerbositySettings,
uiState: {
pendingHistoryItems: [
{
@@ -406,6 +490,7 @@ describe('<ToolGroupMessage />', () => {
</Scrollable>,
{
config: baseMockConfig,
settings: fullVerbositySettings,
uiState: {
pendingHistoryItems: [
{
@@ -439,6 +524,7 @@ describe('<ToolGroupMessage />', () => {
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{
config: baseMockConfig,
settings: fullVerbositySettings,
uiState: {
pendingHistoryItems: [
{
@@ -468,6 +554,7 @@ describe('<ToolGroupMessage />', () => {
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{
config: baseMockConfig,
settings: fullVerbositySettings,
uiState: {
pendingHistoryItems: [
{
@@ -510,6 +597,7 @@ describe('<ToolGroupMessage />', () => {
/>,
{
config: baseMockConfig,
settings: fullVerbositySettings,
uiState: {
pendingHistoryItems: [
{
@@ -569,7 +657,7 @@ describe('<ToolGroupMessage />', () => {
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{ config: baseMockConfig },
{ config: baseMockConfig, settings: fullVerbositySettings },
);
await waitUntilReady();
@@ -599,7 +687,7 @@ describe('<ToolGroupMessage />', () => {
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{ config: baseMockConfig },
{ config: baseMockConfig, settings: fullVerbositySettings },
);
await waitUntilReady();
@@ -627,7 +715,7 @@ describe('<ToolGroupMessage />', () => {
toolCalls={toolCalls}
borderBottom={false}
/>,
{ config: baseMockConfig },
{ config: baseMockConfig, settings: fullVerbositySettings },
);
// AskUser tools in progress are rendered by AskUserDialog, so we expect nothing.
await waitUntilReady();
@@ -665,7 +753,7 @@ describe('<ToolGroupMessage />', () => {
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
{ config: baseMockConfig },
{ config: baseMockConfig, settings: fullVerbositySettings },
);
await waitUntilReady();
@@ -697,6 +785,7 @@ describe('<ToolGroupMessage />', () => {
/>,
{
config: baseMockConfig,
settings: fullVerbositySettings,
useAlternateBuffer: true,
uiState: {
constrainHeight: true,
@@ -728,6 +817,7 @@ describe('<ToolGroupMessage />', () => {
/>,
{
config: baseMockConfig,
settings: fullVerbositySettings,
useAlternateBuffer: true,
uiState: {
constrainHeight: true,
@@ -760,6 +850,7 @@ describe('<ToolGroupMessage />', () => {
/>,
{
config: baseMockConfig,
settings: fullVerbositySettings,
useAlternateBuffer: true,
uiState: {
constrainHeight: true,
@@ -791,6 +882,7 @@ describe('<ToolGroupMessage />', () => {
/>,
{
config: baseMockConfig,
settings: fullVerbositySettings,
useAlternateBuffer: true,
uiState: {
constrainHeight: true,
@@ -818,6 +910,7 @@ describe('<ToolGroupMessage />', () => {
/>,
{
config: baseMockConfig,
settings: fullVerbositySettings,
useAlternateBuffer: true,
uiState: {
constrainHeight: false,
@@ -850,6 +943,7 @@ describe('<ToolGroupMessage />', () => {
/>,
{
config: baseMockConfig,
settings: fullVerbositySettings,
useAlternateBuffer: true,
uiState: {
constrainHeight: true,

View File

@@ -18,7 +18,10 @@ import { ShellToolMessage } from './ShellToolMessage.js';
import { theme } from '../../semantic-colors.js';
import { useConfig } from '../../contexts/ConfigContext.js';
import { isShellTool, isThisShellFocused } from './ToolShared.js';
import { shouldHideToolCall } from '@google/gemini-cli-core';
import {
shouldHideToolCall,
CoreToolCallStatus,
} from '@google/gemini-cli-core';
import { ShowMoreLines } from '../ShowMoreLines.js';
import { useUIState } from '../../contexts/UIStateContext.js';
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
@@ -27,6 +30,7 @@ import {
calculateToolContentMaxLines,
} from '../../utils/toolLayoutUtils.js';
import { getToolGroupBorderAppearance } from '../../utils/borderStyles.js';
import { useSettings } from '../../contexts/SettingsContext.js';
interface ToolGroupMessageProps {
item: HistoryItem | HistoryItemWithoutId;
@@ -51,19 +55,29 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
borderBottom: borderBottomOverride,
isExpandable,
}) => {
const settings = useSettings();
const isLowErrorVerbosity = settings.merged.ui?.errorVerbosity !== 'full';
// Filter out tool calls that should be hidden (e.g. in-progress Ask User, or Plan Mode operations).
const toolCalls = useMemo(
() =>
allToolCalls.filter(
(t) =>
!shouldHideToolCall({
displayName: t.name,
status: t.status,
approvalMode: t.approvalMode,
hasResultDisplay: !!t.resultDisplay,
}),
),
[allToolCalls],
allToolCalls.filter((t) => {
if (
isLowErrorVerbosity &&
t.status === CoreToolCallStatus.Error &&
!t.isClientInitiated
) {
return false;
}
return !shouldHideToolCall({
displayName: t.name,
status: t.status,
approvalMode: t.approvalMode,
hasResultDisplay: !!t.resultDisplay,
});
}),
[allToolCalls, isLowErrorVerbosity],
);
const config = useConfig();

View File

@@ -50,10 +50,14 @@ describe('ToolResultDisplay Overflow', () => {
await waitUntilReady();
// ResizeObserver might take a tick, though ToolGroupMessage calculates overflow synchronously
// In ASB mode the overflow hint can render before the scroll position
// settles. Wait for both the hint and the tail of the content so this
// snapshot is deterministic across slower CI runners.
await waitFor(() => {
const frame = lastFrame();
expect(frame.toLowerCase()).toContain('press ctrl+o to show more lines');
expect(frame).toBeDefined();
expect(frame?.toLowerCase()).toContain('press ctrl+o to show more lines');
expect(frame).toContain('line 50');
});
const frame = lastFrame();