mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 05:42:54 -07:00
Add low/full CLI error verbosity mode for cleaner UI (#20399)
This commit is contained in:
@@ -72,6 +72,7 @@ they appear in the UI.
|
|||||||
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
|
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |
|
||||||
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
|
| Show Spinner | `ui.showSpinner` | Show the spinner during operations. | `true` |
|
||||||
| Loading Phrases | `ui.loadingPhrases` | What to show while the model is working: tips, witty comments, both, or nothing. | `"tips"` |
|
| Loading Phrases | `ui.loadingPhrases` | What to show while the model is working: tips, witty comments, both, or nothing. | `"tips"` |
|
||||||
|
| Error Verbosity | `ui.errorVerbosity` | Controls whether recoverable errors are hidden (low) or fully shown (full). | `"low"` |
|
||||||
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
|
| Screen Reader Mode | `ui.accessibility.screenReader` | Render output in plain-text to be more screen reader accessible | `false` |
|
||||||
|
|
||||||
### IDE
|
### IDE
|
||||||
|
|||||||
@@ -322,6 +322,12 @@ their corresponding top-level category object in your `settings.json` file.
|
|||||||
- **Default:** `"tips"`
|
- **Default:** `"tips"`
|
||||||
- **Values:** `"tips"`, `"witty"`, `"all"`, `"off"`
|
- **Values:** `"tips"`, `"witty"`, `"all"`, `"off"`
|
||||||
|
|
||||||
|
- **`ui.errorVerbosity`** (enum):
|
||||||
|
- **Description:** Controls whether recoverable errors are hidden (low) or
|
||||||
|
fully shown (full).
|
||||||
|
- **Default:** `"low"`
|
||||||
|
- **Values:** `"low"`, `"full"`
|
||||||
|
|
||||||
- **`ui.customWittyPhrases`** (array):
|
- **`ui.customWittyPhrases`** (array):
|
||||||
- **Description:** Custom witty phrases to display during loading. When
|
- **Description:** Custom witty phrases to display during loading. When
|
||||||
provided, the CLI cycles through these instead of the defaults.
|
provided, the CLI cycles through these instead of the defaults.
|
||||||
|
|||||||
@@ -96,6 +96,14 @@ describe('SettingsSchema', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should have errorVerbosity enum property', () => {
|
||||||
|
const definition = getSettingsSchema().ui?.properties?.errorVerbosity;
|
||||||
|
expect(definition).toBeDefined();
|
||||||
|
expect(definition?.type).toBe('enum');
|
||||||
|
expect(definition?.default).toBe('low');
|
||||||
|
expect(definition?.options?.map((o) => o.value)).toEqual(['low', 'full']);
|
||||||
|
});
|
||||||
|
|
||||||
it('should have checkpointing nested properties', () => {
|
it('should have checkpointing nested properties', () => {
|
||||||
expect(
|
expect(
|
||||||
getSettingsSchema().general?.properties?.checkpointing.properties
|
getSettingsSchema().general?.properties?.checkpointing.properties
|
||||||
|
|||||||
@@ -719,6 +719,20 @@ const SETTINGS_SCHEMA = {
|
|||||||
{ value: 'off', label: 'Off' },
|
{ value: 'off', label: 'Off' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
errorVerbosity: {
|
||||||
|
type: 'enum',
|
||||||
|
label: 'Error Verbosity',
|
||||||
|
category: 'UI',
|
||||||
|
requiresRestart: false,
|
||||||
|
default: 'low',
|
||||||
|
description:
|
||||||
|
'Controls whether recoverable errors are hidden (low) or fully shown (full).',
|
||||||
|
showInDialog: true,
|
||||||
|
options: [
|
||||||
|
{ value: 'low', label: 'Low' },
|
||||||
|
{ value: 'full', label: 'Full' },
|
||||||
|
],
|
||||||
|
},
|
||||||
customWittyPhrases: {
|
customWittyPhrases: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
label: 'Custom Witty Phrases',
|
label: 'Custom Witty Phrases',
|
||||||
|
|||||||
@@ -712,6 +712,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
|||||||
settings,
|
settings,
|
||||||
setModelSwitchedFromQuotaError,
|
setModelSwitchedFromQuotaError,
|
||||||
onShowAuthSelection: () => setAuthState(AuthState.Updating),
|
onShowAuthSelection: () => setAuthState(AuthState.Updating),
|
||||||
|
errorVerbosity: settings.merged.ui.errorVerbosity,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Derive auth state variables for backward compatibility with UIStateContext
|
// Derive auth state variables for backward compatibility with UIStateContext
|
||||||
@@ -1688,6 +1689,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
retryStatus,
|
retryStatus,
|
||||||
loadingPhrasesMode: settings.merged.ui.loadingPhrases,
|
loadingPhrasesMode: settings.merged.ui.loadingPhrases,
|
||||||
customWittyPhrases: settings.merged.ui.customWittyPhrases,
|
customWittyPhrases: settings.merged.ui.customWittyPhrases,
|
||||||
|
errorVerbosity: settings.merged.ui.errorVerbosity,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleGlobalKeypress = useCallback(
|
const handleGlobalKeypress = useCallback(
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from '../../test-utils/render.js';
|
import { renderWithProviders } from '../../test-utils/render.js';
|
||||||
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
|
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
|
||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import type { ConsoleMessageItem } from '../types.js';
|
import type { ConsoleMessageItem } from '../types.js';
|
||||||
import { Box } from 'ink';
|
import { Box } from 'ink';
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
|
import { createMockSettings } from '../../test-utils/settings.js';
|
||||||
|
|
||||||
vi.mock('./shared/ScrollableList.js', () => ({
|
vi.mock('./shared/ScrollableList.js', () => ({
|
||||||
ScrollableList: ({
|
ScrollableList: ({
|
||||||
@@ -29,13 +30,18 @@ vi.mock('./shared/ScrollableList.js', () => ({
|
|||||||
|
|
||||||
describe('DetailedMessagesDisplay', () => {
|
describe('DetailedMessagesDisplay', () => {
|
||||||
it('renders nothing when messages are empty', async () => {
|
it('renders nothing when messages are empty', async () => {
|
||||||
const { lastFrame, waitUntilReady, unmount } = render(
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||||
<DetailedMessagesDisplay
|
<DetailedMessagesDisplay
|
||||||
messages={[]}
|
messages={[]}
|
||||||
maxHeight={10}
|
maxHeight={10}
|
||||||
width={80}
|
width={80}
|
||||||
hasFocus={false}
|
hasFocus={false}
|
||||||
/>,
|
/>,
|
||||||
|
{
|
||||||
|
settings: createMockSettings({
|
||||||
|
merged: { ui: { errorVerbosity: 'full' } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
await waitUntilReady();
|
await waitUntilReady();
|
||||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||||
@@ -50,13 +56,18 @@ describe('DetailedMessagesDisplay', () => {
|
|||||||
{ type: 'debug', content: 'Debug message', count: 1 },
|
{ type: 'debug', content: 'Debug message', count: 1 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const { lastFrame, waitUntilReady, unmount } = render(
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||||
<DetailedMessagesDisplay
|
<DetailedMessagesDisplay
|
||||||
messages={messages}
|
messages={messages}
|
||||||
maxHeight={20}
|
maxHeight={20}
|
||||||
width={80}
|
width={80}
|
||||||
hasFocus={true}
|
hasFocus={true}
|
||||||
/>,
|
/>,
|
||||||
|
{
|
||||||
|
settings: createMockSettings({
|
||||||
|
merged: { ui: { errorVerbosity: 'full' } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
await waitUntilReady();
|
await waitUntilReady();
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
@@ -65,18 +76,69 @@ describe('DetailedMessagesDisplay', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides the F12 hint in low error verbosity mode', async () => {
|
||||||
|
const messages: ConsoleMessageItem[] = [
|
||||||
|
{ type: 'error', content: 'Error message', count: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||||
|
<DetailedMessagesDisplay
|
||||||
|
messages={messages}
|
||||||
|
maxHeight={20}
|
||||||
|
width={80}
|
||||||
|
hasFocus={true}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
settings: createMockSettings({
|
||||||
|
merged: { ui: { errorVerbosity: 'low' } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await waitUntilReady();
|
||||||
|
expect(lastFrame()).not.toContain('(F12 to close)');
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the F12 hint in full error verbosity mode', async () => {
|
||||||
|
const messages: ConsoleMessageItem[] = [
|
||||||
|
{ type: 'error', content: 'Error message', count: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||||
|
<DetailedMessagesDisplay
|
||||||
|
messages={messages}
|
||||||
|
maxHeight={20}
|
||||||
|
width={80}
|
||||||
|
hasFocus={true}
|
||||||
|
/>,
|
||||||
|
{
|
||||||
|
settings: createMockSettings({
|
||||||
|
merged: { ui: { errorVerbosity: 'full' } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await waitUntilReady();
|
||||||
|
expect(lastFrame()).toContain('(F12 to close)');
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
it('renders message counts', async () => {
|
it('renders message counts', async () => {
|
||||||
const messages: ConsoleMessageItem[] = [
|
const messages: ConsoleMessageItem[] = [
|
||||||
{ type: 'log', content: 'Repeated message', count: 5 },
|
{ type: 'log', content: 'Repeated message', count: 5 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const { lastFrame, waitUntilReady, unmount } = render(
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||||
<DetailedMessagesDisplay
|
<DetailedMessagesDisplay
|
||||||
messages={messages}
|
messages={messages}
|
||||||
maxHeight={10}
|
maxHeight={10}
|
||||||
width={80}
|
width={80}
|
||||||
hasFocus={false}
|
hasFocus={false}
|
||||||
/>,
|
/>,
|
||||||
|
{
|
||||||
|
settings: createMockSettings({
|
||||||
|
merged: { ui: { errorVerbosity: 'full' } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
await waitUntilReady();
|
await waitUntilReady();
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
ScrollableList,
|
ScrollableList,
|
||||||
type ScrollableListRef,
|
type ScrollableListRef,
|
||||||
} from './shared/ScrollableList.js';
|
} from './shared/ScrollableList.js';
|
||||||
|
import { useConfig } from '../contexts/ConfigContext.js';
|
||||||
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
|
|
||||||
interface DetailedMessagesDisplayProps {
|
interface DetailedMessagesDisplayProps {
|
||||||
messages: ConsoleMessageItem[];
|
messages: ConsoleMessageItem[];
|
||||||
@@ -27,6 +29,10 @@ export const DetailedMessagesDisplay: React.FC<
|
|||||||
DetailedMessagesDisplayProps
|
DetailedMessagesDisplayProps
|
||||||
> = ({ messages, maxHeight, width, hasFocus }) => {
|
> = ({ messages, maxHeight, width, hasFocus }) => {
|
||||||
const scrollableListRef = useRef<ScrollableListRef<ConsoleMessageItem>>(null);
|
const scrollableListRef = useRef<ScrollableListRef<ConsoleMessageItem>>(null);
|
||||||
|
const config = useConfig();
|
||||||
|
const settings = useSettings();
|
||||||
|
const showHotkeyHint =
|
||||||
|
settings.merged.ui.errorVerbosity === 'full' || config.getDebugMode();
|
||||||
|
|
||||||
const borderAndPadding = 3;
|
const borderAndPadding = 3;
|
||||||
|
|
||||||
@@ -65,7 +71,10 @@ export const DetailedMessagesDisplay: React.FC<
|
|||||||
>
|
>
|
||||||
<Box marginBottom={1}>
|
<Box marginBottom={1}>
|
||||||
<Text bold color={theme.text.primary}>
|
<Text bold color={theme.text.primary}>
|
||||||
Debug Console <Text color={theme.text.secondary}>(F12 to close)</Text>
|
Debug Console{' '}
|
||||||
|
{showHotkeyHint && (
|
||||||
|
<Text color={theme.text.secondary}>(F12 to close)</Text>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box height={maxHeight} width={width - borderAndPadding}>
|
<Box height={maxHeight} width={width - borderAndPadding}>
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|||||||
import { renderWithProviders } from '../../test-utils/render.js';
|
import { renderWithProviders } from '../../test-utils/render.js';
|
||||||
import { createMockSettings } from '../../test-utils/settings.js';
|
import { createMockSettings } from '../../test-utils/settings.js';
|
||||||
import { Footer } from './Footer.js';
|
import { Footer } from './Footer.js';
|
||||||
import { tildeifyPath, ToolCallDecision } from '@google/gemini-cli-core';
|
import {
|
||||||
|
makeFakeConfig,
|
||||||
|
tildeifyPath,
|
||||||
|
ToolCallDecision,
|
||||||
|
} from '@google/gemini-cli-core';
|
||||||
import type { SessionStatsState } from '../contexts/SessionContext.js';
|
import type { SessionStatsState } from '../contexts/SessionContext.js';
|
||||||
|
|
||||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||||
@@ -503,6 +507,75 @@ describe('<Footer />', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('error summary visibility', () => {
|
||||||
|
it('hides error summary in low verbosity mode', async () => {
|
||||||
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||||
|
<Footer />,
|
||||||
|
{
|
||||||
|
width: 120,
|
||||||
|
uiState: {
|
||||||
|
sessionStats: mockSessionStats,
|
||||||
|
errorCount: 2,
|
||||||
|
showErrorDetails: false,
|
||||||
|
},
|
||||||
|
settings: createMockSettings({
|
||||||
|
merged: { ui: { errorVerbosity: 'low' } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await waitUntilReady();
|
||||||
|
expect(lastFrame()).not.toContain('F12 for details');
|
||||||
|
expect(lastFrame()).not.toContain('2 errors');
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error summary in full verbosity mode', async () => {
|
||||||
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||||
|
<Footer />,
|
||||||
|
{
|
||||||
|
width: 120,
|
||||||
|
uiState: {
|
||||||
|
sessionStats: mockSessionStats,
|
||||||
|
errorCount: 2,
|
||||||
|
showErrorDetails: false,
|
||||||
|
},
|
||||||
|
settings: createMockSettings({
|
||||||
|
merged: { ui: { errorVerbosity: 'full' } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await waitUntilReady();
|
||||||
|
expect(lastFrame()).toContain('F12 for details');
|
||||||
|
expect(lastFrame()).toContain('2 errors');
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows error summary in debug mode even when verbosity is low', async () => {
|
||||||
|
const debugConfig = makeFakeConfig();
|
||||||
|
vi.spyOn(debugConfig, 'getDebugMode').mockReturnValue(true);
|
||||||
|
|
||||||
|
const { lastFrame, waitUntilReady, unmount } = renderWithProviders(
|
||||||
|
<Footer />,
|
||||||
|
{
|
||||||
|
width: 120,
|
||||||
|
config: debugConfig,
|
||||||
|
uiState: {
|
||||||
|
sessionStats: mockSessionStats,
|
||||||
|
errorCount: 1,
|
||||||
|
showErrorDetails: false,
|
||||||
|
},
|
||||||
|
settings: createMockSettings({
|
||||||
|
merged: { ui: { errorVerbosity: 'low' } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await waitUntilReady();
|
||||||
|
expect(lastFrame()).toContain('F12 for details');
|
||||||
|
expect(lastFrame()).toContain('1 error');
|
||||||
|
unmount();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fallback mode display', () => {
|
describe('fallback mode display', () => {
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ export const Footer: React.FC = () => {
|
|||||||
|
|
||||||
const showMemoryUsage =
|
const showMemoryUsage =
|
||||||
config.getDebugMode() || settings.merged.ui.showMemoryUsage;
|
config.getDebugMode() || settings.merged.ui.showMemoryUsage;
|
||||||
|
const isFullErrorVerbosity = settings.merged.ui.errorVerbosity === 'full';
|
||||||
|
const showErrorSummary =
|
||||||
|
!showErrorDetails && errorCount > 0 && (isFullErrorVerbosity || debugMode);
|
||||||
const hideCWD = settings.merged.ui.footer.hideCWD;
|
const hideCWD = settings.merged.ui.footer.hideCWD;
|
||||||
const hideSandboxStatus = settings.merged.ui.footer.hideSandboxStatus;
|
const hideSandboxStatus = settings.merged.ui.footer.hideSandboxStatus;
|
||||||
const hideModelInfo = settings.merged.ui.footer.hideModelInfo;
|
const hideModelInfo = settings.merged.ui.footer.hideModelInfo;
|
||||||
@@ -180,7 +183,7 @@ export const Footer: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{!showErrorDetails && errorCount > 0 && (
|
{showErrorSummary && (
|
||||||
<Box paddingLeft={1} flexDirection="row">
|
<Box paddingLeft={1} flexDirection="row">
|
||||||
<Text color={theme.ui.comment}>| </Text>
|
<Text color={theme.ui.comment}>| </Text>
|
||||||
<ConsoleSummaryDisplay errorCount={errorCount} />
|
<ConsoleSummaryDisplay errorCount={errorCount} />
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
GLOB_DISPLAY_NAME,
|
GLOB_DISPLAY_NAME,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
|
import { createMockSettings } from '../../../test-utils/settings.js';
|
||||||
|
|
||||||
describe('<ToolGroupMessage />', () => {
|
describe('<ToolGroupMessage />', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -64,6 +65,11 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
ideMode: false,
|
ideMode: false,
|
||||||
enableInteractiveShell: true,
|
enableInteractiveShell: true,
|
||||||
});
|
});
|
||||||
|
const fullVerbositySettings = createMockSettings({
|
||||||
|
merged: {
|
||||||
|
ui: { errorVerbosity: 'full' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('Golden Snapshots', () => {
|
describe('Golden Snapshots', () => {
|
||||||
it('renders single successful tool call', async () => {
|
it('renders single successful tool call', async () => {
|
||||||
@@ -73,6 +79,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
uiState: {
|
uiState: {
|
||||||
pendingHistoryItems: [
|
pendingHistoryItems: [
|
||||||
{
|
{
|
||||||
@@ -104,7 +111,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
|
|
||||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||||
{ config: baseMockConfig },
|
{ config: baseMockConfig, settings: fullVerbositySettings },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Should render nothing because all tools in the group are confirming
|
// Should render nothing because all tools in the group are confirming
|
||||||
@@ -140,6 +147,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
uiState: {
|
uiState: {
|
||||||
pendingHistoryItems: [
|
pendingHistoryItems: [
|
||||||
{
|
{
|
||||||
@@ -160,6 +168,76 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
unmount();
|
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 () => {
|
it('renders mixed tool calls including shell command', async () => {
|
||||||
const toolCalls = [
|
const toolCalls = [
|
||||||
createToolCall({
|
createToolCall({
|
||||||
@@ -187,6 +265,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
uiState: {
|
uiState: {
|
||||||
pendingHistoryItems: [
|
pendingHistoryItems: [
|
||||||
{
|
{
|
||||||
@@ -233,6 +312,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
uiState: {
|
uiState: {
|
||||||
pendingHistoryItems: [
|
pendingHistoryItems: [
|
||||||
{
|
{
|
||||||
@@ -266,6 +346,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
uiState: {
|
uiState: {
|
||||||
pendingHistoryItems: [
|
pendingHistoryItems: [
|
||||||
{
|
{
|
||||||
@@ -288,6 +369,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
uiState: {
|
uiState: {
|
||||||
pendingHistoryItems: [
|
pendingHistoryItems: [
|
||||||
{
|
{
|
||||||
@@ -326,6 +408,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
</Scrollable>,
|
</Scrollable>,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
uiState: {
|
uiState: {
|
||||||
pendingHistoryItems: [
|
pendingHistoryItems: [
|
||||||
{
|
{
|
||||||
@@ -356,6 +439,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
uiState: {
|
uiState: {
|
||||||
pendingHistoryItems: [
|
pendingHistoryItems: [
|
||||||
{
|
{
|
||||||
@@ -406,6 +490,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
</Scrollable>,
|
</Scrollable>,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
uiState: {
|
uiState: {
|
||||||
pendingHistoryItems: [
|
pendingHistoryItems: [
|
||||||
{
|
{
|
||||||
@@ -439,6 +524,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
uiState: {
|
uiState: {
|
||||||
pendingHistoryItems: [
|
pendingHistoryItems: [
|
||||||
{
|
{
|
||||||
@@ -468,6 +554,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
uiState: {
|
uiState: {
|
||||||
pendingHistoryItems: [
|
pendingHistoryItems: [
|
||||||
{
|
{
|
||||||
@@ -510,6 +597,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
uiState: {
|
uiState: {
|
||||||
pendingHistoryItems: [
|
pendingHistoryItems: [
|
||||||
{
|
{
|
||||||
@@ -569,7 +657,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
|
|
||||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||||
{ config: baseMockConfig },
|
{ config: baseMockConfig, settings: fullVerbositySettings },
|
||||||
);
|
);
|
||||||
await waitUntilReady();
|
await waitUntilReady();
|
||||||
|
|
||||||
@@ -599,7 +687,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
|
|
||||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||||
{ config: baseMockConfig },
|
{ config: baseMockConfig, settings: fullVerbositySettings },
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitUntilReady();
|
await waitUntilReady();
|
||||||
@@ -627,7 +715,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
toolCalls={toolCalls}
|
toolCalls={toolCalls}
|
||||||
borderBottom={false}
|
borderBottom={false}
|
||||||
/>,
|
/>,
|
||||||
{ config: baseMockConfig },
|
{ config: baseMockConfig, settings: fullVerbositySettings },
|
||||||
);
|
);
|
||||||
// AskUser tools in progress are rendered by AskUserDialog, so we expect nothing.
|
// AskUser tools in progress are rendered by AskUserDialog, so we expect nothing.
|
||||||
await waitUntilReady();
|
await waitUntilReady();
|
||||||
@@ -665,7 +753,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
|
|
||||||
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
const { lastFrame, unmount, waitUntilReady } = renderWithProviders(
|
||||||
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
<ToolGroupMessage {...baseProps} item={item} toolCalls={toolCalls} />,
|
||||||
{ config: baseMockConfig },
|
{ config: baseMockConfig, settings: fullVerbositySettings },
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitUntilReady();
|
await waitUntilReady();
|
||||||
@@ -697,6 +785,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
useAlternateBuffer: true,
|
useAlternateBuffer: true,
|
||||||
uiState: {
|
uiState: {
|
||||||
constrainHeight: true,
|
constrainHeight: true,
|
||||||
@@ -728,6 +817,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
useAlternateBuffer: true,
|
useAlternateBuffer: true,
|
||||||
uiState: {
|
uiState: {
|
||||||
constrainHeight: true,
|
constrainHeight: true,
|
||||||
@@ -760,6 +850,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
useAlternateBuffer: true,
|
useAlternateBuffer: true,
|
||||||
uiState: {
|
uiState: {
|
||||||
constrainHeight: true,
|
constrainHeight: true,
|
||||||
@@ -791,6 +882,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
useAlternateBuffer: true,
|
useAlternateBuffer: true,
|
||||||
uiState: {
|
uiState: {
|
||||||
constrainHeight: true,
|
constrainHeight: true,
|
||||||
@@ -818,6 +910,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
useAlternateBuffer: true,
|
useAlternateBuffer: true,
|
||||||
uiState: {
|
uiState: {
|
||||||
constrainHeight: false,
|
constrainHeight: false,
|
||||||
@@ -850,6 +943,7 @@ describe('<ToolGroupMessage />', () => {
|
|||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
config: baseMockConfig,
|
config: baseMockConfig,
|
||||||
|
settings: fullVerbositySettings,
|
||||||
useAlternateBuffer: true,
|
useAlternateBuffer: true,
|
||||||
uiState: {
|
uiState: {
|
||||||
constrainHeight: true,
|
constrainHeight: true,
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ import { ShellToolMessage } from './ShellToolMessage.js';
|
|||||||
import { theme } from '../../semantic-colors.js';
|
import { theme } from '../../semantic-colors.js';
|
||||||
import { useConfig } from '../../contexts/ConfigContext.js';
|
import { useConfig } from '../../contexts/ConfigContext.js';
|
||||||
import { isShellTool, isThisShellFocused } from './ToolShared.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 { ShowMoreLines } from '../ShowMoreLines.js';
|
||||||
import { useUIState } from '../../contexts/UIStateContext.js';
|
import { useUIState } from '../../contexts/UIStateContext.js';
|
||||||
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
|
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
|
||||||
@@ -27,6 +30,7 @@ import {
|
|||||||
calculateToolContentMaxLines,
|
calculateToolContentMaxLines,
|
||||||
} from '../../utils/toolLayoutUtils.js';
|
} from '../../utils/toolLayoutUtils.js';
|
||||||
import { getToolGroupBorderAppearance } from '../../utils/borderStyles.js';
|
import { getToolGroupBorderAppearance } from '../../utils/borderStyles.js';
|
||||||
|
import { useSettings } from '../../contexts/SettingsContext.js';
|
||||||
|
|
||||||
interface ToolGroupMessageProps {
|
interface ToolGroupMessageProps {
|
||||||
item: HistoryItem | HistoryItemWithoutId;
|
item: HistoryItem | HistoryItemWithoutId;
|
||||||
@@ -51,19 +55,29 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
|||||||
borderBottom: borderBottomOverride,
|
borderBottom: borderBottomOverride,
|
||||||
isExpandable,
|
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).
|
// Filter out tool calls that should be hidden (e.g. in-progress Ask User, or Plan Mode operations).
|
||||||
const toolCalls = useMemo(
|
const toolCalls = useMemo(
|
||||||
() =>
|
() =>
|
||||||
allToolCalls.filter(
|
allToolCalls.filter((t) => {
|
||||||
(t) =>
|
if (
|
||||||
!shouldHideToolCall({
|
isLowErrorVerbosity &&
|
||||||
displayName: t.name,
|
t.status === CoreToolCallStatus.Error &&
|
||||||
status: t.status,
|
!t.isClientInitiated
|
||||||
approvalMode: t.approvalMode,
|
) {
|
||||||
hasResultDisplay: !!t.resultDisplay,
|
return false;
|
||||||
}),
|
}
|
||||||
),
|
|
||||||
[allToolCalls],
|
return !shouldHideToolCall({
|
||||||
|
displayName: t.name,
|
||||||
|
status: t.status,
|
||||||
|
approvalMode: t.approvalMode,
|
||||||
|
hasResultDisplay: !!t.resultDisplay,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
[allToolCalls, isLowErrorVerbosity],
|
||||||
);
|
);
|
||||||
|
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
|
|||||||
@@ -50,10 +50,14 @@ describe('ToolResultDisplay Overflow', () => {
|
|||||||
|
|
||||||
await waitUntilReady();
|
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(() => {
|
await waitFor(() => {
|
||||||
const frame = lastFrame();
|
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();
|
const frame = lastFrame();
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ async function readMcpResources(
|
|||||||
name: `resources/read (${resource.serverName})`,
|
name: `resources/read (${resource.serverName})`,
|
||||||
description: resource.uri,
|
description: resource.uri,
|
||||||
status: CoreToolCallStatus.Success,
|
status: CoreToolCallStatus.Success,
|
||||||
|
isClientInitiated: true,
|
||||||
resultDisplay: `Successfully read resource ${resource.uri}`,
|
resultDisplay: `Successfully read resource ${resource.uri}`,
|
||||||
confirmationDetails: undefined,
|
confirmationDetails: undefined,
|
||||||
} as IndividualToolCallDisplay,
|
} as IndividualToolCallDisplay,
|
||||||
@@ -427,6 +428,7 @@ async function readMcpResources(
|
|||||||
name: `resources/read (${resource.serverName})`,
|
name: `resources/read (${resource.serverName})`,
|
||||||
description: resource.uri,
|
description: resource.uri,
|
||||||
status: CoreToolCallStatus.Error,
|
status: CoreToolCallStatus.Error,
|
||||||
|
isClientInitiated: true,
|
||||||
resultDisplay: `Error reading resource ${resource.uri}: ${getErrorMessage(error)}`,
|
resultDisplay: `Error reading resource ${resource.uri}: ${getErrorMessage(error)}`,
|
||||||
confirmationDetails: undefined,
|
confirmationDetails: undefined,
|
||||||
} as IndividualToolCallDisplay,
|
} as IndividualToolCallDisplay,
|
||||||
@@ -506,6 +508,7 @@ async function readLocalFiles(
|
|||||||
name: readManyFilesTool.displayName,
|
name: readManyFilesTool.displayName,
|
||||||
description: invocation.getDescription(),
|
description: invocation.getDescription(),
|
||||||
status: CoreToolCallStatus.Success,
|
status: CoreToolCallStatus.Success,
|
||||||
|
isClientInitiated: true,
|
||||||
resultDisplay:
|
resultDisplay:
|
||||||
result.returnDisplay ||
|
result.returnDisplay ||
|
||||||
`Successfully read: ${fileLabelsForDisplay.join(', ')}`,
|
`Successfully read: ${fileLabelsForDisplay.join(', ')}`,
|
||||||
@@ -565,6 +568,7 @@ async function readLocalFiles(
|
|||||||
invocation?.getDescription() ??
|
invocation?.getDescription() ??
|
||||||
'Error attempting to execute tool to read files',
|
'Error attempting to execute tool to read files',
|
||||||
status: CoreToolCallStatus.Error,
|
status: CoreToolCallStatus.Error,
|
||||||
|
isClientInitiated: true,
|
||||||
resultDisplay: `Error reading files (${fileLabelsForDisplay.join(', ')}): ${getErrorMessage(error)}`,
|
resultDisplay: `Error reading files (${fileLabelsForDisplay.join(', ')}): ${getErrorMessage(error)}`,
|
||||||
confirmationDetails: undefined,
|
confirmationDetails: undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -305,6 +305,7 @@ export const useShellCommandProcessor = (
|
|||||||
name: SHELL_COMMAND_NAME,
|
name: SHELL_COMMAND_NAME,
|
||||||
description: rawQuery,
|
description: rawQuery,
|
||||||
status: CoreToolCallStatus.Executing,
|
status: CoreToolCallStatus.Executing,
|
||||||
|
isClientInitiated: true,
|
||||||
resultDisplay: '',
|
resultDisplay: '',
|
||||||
confirmationDetails: undefined,
|
confirmationDetails: undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -581,6 +581,7 @@ export const useSlashCommandProcessor = (
|
|||||||
name: 'Expansion',
|
name: 'Expansion',
|
||||||
description: 'Command expansion needs shell access',
|
description: 'Command expansion needs shell access',
|
||||||
status: CoreToolCallStatus.AwaitingApproval,
|
status: CoreToolCallStatus.AwaitingApproval,
|
||||||
|
isClientInitiated: true,
|
||||||
resultDisplay: undefined,
|
resultDisplay: undefined,
|
||||||
confirmationDetails,
|
confirmationDetails,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -325,5 +325,33 @@ describe('toolMapping', () => {
|
|||||||
const result = mapToDisplay(toolCall);
|
const result = mapToDisplay(toolCall);
|
||||||
expect(result.tools[0].originalRequestName).toBe('original_tool');
|
expect(result.tools[0].originalRequestName).toBe('original_tool');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('propagates isClientInitiated from tool request', () => {
|
||||||
|
const clientInitiatedTool: ScheduledToolCall = {
|
||||||
|
status: CoreToolCallStatus.Scheduled,
|
||||||
|
request: {
|
||||||
|
...mockRequest,
|
||||||
|
callId: 'call-client',
|
||||||
|
isClientInitiated: true,
|
||||||
|
},
|
||||||
|
tool: mockTool,
|
||||||
|
invocation: mockInvocation,
|
||||||
|
};
|
||||||
|
|
||||||
|
const modelInitiatedTool: ScheduledToolCall = {
|
||||||
|
status: CoreToolCallStatus.Scheduled,
|
||||||
|
request: {
|
||||||
|
...mockRequest,
|
||||||
|
callId: 'call-model',
|
||||||
|
isClientInitiated: false,
|
||||||
|
},
|
||||||
|
tool: mockTool,
|
||||||
|
invocation: mockInvocation,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = mapToDisplay([clientInitiatedTool, modelInitiatedTool]);
|
||||||
|
expect(result.tools[0].isClientInitiated).toBe(true);
|
||||||
|
expect(result.tools[1].isClientInitiated).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export function mapToDisplay(
|
|||||||
return {
|
return {
|
||||||
...baseDisplayProperties,
|
...baseDisplayProperties,
|
||||||
status: call.status,
|
status: call.status,
|
||||||
|
isClientInitiated: !!call.request.isClientInitiated,
|
||||||
resultDisplay,
|
resultDisplay,
|
||||||
confirmationDetails,
|
confirmationDetails,
|
||||||
outputFile,
|
outputFile,
|
||||||
|
|||||||
@@ -335,7 +335,10 @@ describe('useGeminiStream', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mockLoadedSettings: LoadedSettings = {
|
const mockLoadedSettings: LoadedSettings = {
|
||||||
merged: { preferredEditor: 'vscode' },
|
merged: {
|
||||||
|
preferredEditor: 'vscode',
|
||||||
|
ui: { errorVerbosity: 'full' },
|
||||||
|
},
|
||||||
user: { path: '/user/settings.json', settings: {} },
|
user: { path: '/user/settings.json', settings: {} },
|
||||||
workspace: { path: '/workspace/.gemini/settings.json', settings: {} },
|
workspace: { path: '/workspace/.gemini/settings.json', settings: {} },
|
||||||
errors: [],
|
errors: [],
|
||||||
@@ -346,6 +349,7 @@ describe('useGeminiStream', () => {
|
|||||||
const renderTestHook = (
|
const renderTestHook = (
|
||||||
initialToolCalls: TrackedToolCall[] = [],
|
initialToolCalls: TrackedToolCall[] = [],
|
||||||
geminiClient?: any,
|
geminiClient?: any,
|
||||||
|
loadedSettings: LoadedSettings = mockLoadedSettings,
|
||||||
) => {
|
) => {
|
||||||
const client = geminiClient || mockConfig.getGeminiClient();
|
const client = geminiClient || mockConfig.getGeminiClient();
|
||||||
let lastToolCalls = initialToolCalls;
|
let lastToolCalls = initialToolCalls;
|
||||||
@@ -360,7 +364,7 @@ describe('useGeminiStream', () => {
|
|||||||
cmd: PartListUnion,
|
cmd: PartListUnion,
|
||||||
) => Promise<SlashCommandProcessorResult | false>,
|
) => Promise<SlashCommandProcessorResult | false>,
|
||||||
shellModeActive: false,
|
shellModeActive: false,
|
||||||
loadedSettings: mockLoadedSettings,
|
loadedSettings,
|
||||||
toolCalls: initialToolCalls,
|
toolCalls: initialToolCalls,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -969,6 +973,93 @@ describe('useGeminiStream', () => {
|
|||||||
// Streaming state should be Idle
|
// Streaming state should be Idle
|
||||||
expect(result.current.streamingState).toBe(StreamingState.Idle);
|
expect(result.current.streamingState).toBe(StreamingState.Idle);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const infoTexts = mockAddItem.mock.calls.map(
|
||||||
|
([item]) => (item as { text?: string }).text ?? '',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
infoTexts.some((text) =>
|
||||||
|
text.includes(
|
||||||
|
'Some internal tool attempts failed before this final error',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
infoTexts.some((text) =>
|
||||||
|
text.includes('This request failed. Press F12 for diagnostics'),
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a compact suppressed-error note before STOP_EXECUTION terminal info in low verbosity mode', async () => {
|
||||||
|
const stopExecutionToolCalls: TrackedToolCall[] = [
|
||||||
|
{
|
||||||
|
request: {
|
||||||
|
callId: 'stop-call',
|
||||||
|
name: 'stopTool',
|
||||||
|
args: {},
|
||||||
|
isClientInitiated: false,
|
||||||
|
prompt_id: 'prompt-id-stop',
|
||||||
|
},
|
||||||
|
status: CoreToolCallStatus.Error,
|
||||||
|
response: {
|
||||||
|
callId: 'stop-call',
|
||||||
|
responseParts: [{ text: 'error occurred' }],
|
||||||
|
errorType: ToolErrorType.STOP_EXECUTION,
|
||||||
|
error: new Error('Stop reason from hook'),
|
||||||
|
resultDisplay: undefined,
|
||||||
|
},
|
||||||
|
responseSubmittedToGemini: false,
|
||||||
|
tool: {
|
||||||
|
displayName: 'stop tool',
|
||||||
|
},
|
||||||
|
invocation: {
|
||||||
|
getDescription: () => `Mock description`,
|
||||||
|
} as unknown as AnyToolInvocation,
|
||||||
|
} as unknown as TrackedCompletedToolCall,
|
||||||
|
];
|
||||||
|
const lowVerbositySettings = {
|
||||||
|
...mockLoadedSettings,
|
||||||
|
merged: {
|
||||||
|
...mockLoadedSettings.merged,
|
||||||
|
ui: { errorVerbosity: 'low' },
|
||||||
|
},
|
||||||
|
} as LoadedSettings;
|
||||||
|
const client = new MockedGeminiClientClass(mockConfig);
|
||||||
|
|
||||||
|
const { result } = renderTestHook([], client, lowVerbositySettings);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
if (capturedOnComplete) {
|
||||||
|
await capturedOnComplete(stopExecutionToolCalls);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockMarkToolsAsSubmitted).toHaveBeenCalledWith(['stop-call']);
|
||||||
|
expect(mockSendMessageStream).not.toHaveBeenCalled();
|
||||||
|
expect(result.current.streamingState).toBe(StreamingState.Idle);
|
||||||
|
});
|
||||||
|
|
||||||
|
const infoTexts = mockAddItem.mock.calls.map(
|
||||||
|
([item]) => (item as { text?: string }).text ?? '',
|
||||||
|
);
|
||||||
|
const noteIndex = infoTexts.findIndex((text) =>
|
||||||
|
text.includes(
|
||||||
|
'Some internal tool attempts failed before this final error',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const stopIndex = infoTexts.findIndex((text) =>
|
||||||
|
text.includes('Agent execution stopped: Stop reason from hook'),
|
||||||
|
);
|
||||||
|
const failureHintIndex = infoTexts.findIndex((text) =>
|
||||||
|
text.includes('This request failed. Press F12 for diagnostics'),
|
||||||
|
);
|
||||||
|
expect(noteIndex).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(stopIndex).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(failureHintIndex).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(noteIndex).toBeLessThan(stopIndex);
|
||||||
|
expect(stopIndex).toBeLessThan(failureHintIndex);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should group multiple cancelled tool call responses into a single history entry', async () => {
|
it('should group multiple cancelled tool call responses into a single history entry', async () => {
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ enum StreamProcessingStatus {
|
|||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SUPPRESSED_TOOL_ERRORS_NOTE =
|
||||||
|
'Some internal tool attempts failed before this final error. Press F12 for diagnostics, or set ui.errorVerbosity to full for full details.';
|
||||||
|
const LOW_VERBOSITY_FAILURE_NOTE =
|
||||||
|
'This request failed. Press F12 for diagnostics, or set ui.errorVerbosity to full for full details.';
|
||||||
|
|
||||||
function isShellToolData(data: unknown): data is ShellToolData {
|
function isShellToolData(data: unknown): data is ShellToolData {
|
||||||
if (typeof data !== 'object' || data === null) {
|
if (typeof data !== 'object' || data === null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -202,6 +207,10 @@ export const useGeminiStream = (
|
|||||||
const [retryStatus, setRetryStatus] = useState<RetryAttemptPayload | null>(
|
const [retryStatus, setRetryStatus] = useState<RetryAttemptPayload | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const isLowErrorVerbosity = settings.merged.ui?.errorVerbosity !== 'full';
|
||||||
|
const suppressedToolErrorCountRef = useRef(0);
|
||||||
|
const suppressedToolErrorNoteShownRef = useRef(false);
|
||||||
|
const lowVerbosityFailureNoteShownRef = useRef(false);
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
const turnCancelledRef = useRef(false);
|
const turnCancelledRef = useRef(false);
|
||||||
const activeQueryIdRef = useRef<string | null>(null);
|
const activeQueryIdRef = useRef<string | null>(null);
|
||||||
@@ -559,6 +568,51 @@ export const useGeminiStream = (
|
|||||||
}
|
}
|
||||||
}, [isResponding]);
|
}, [isResponding]);
|
||||||
|
|
||||||
|
const maybeAddSuppressedToolErrorNote = useCallback(
|
||||||
|
(userMessageTimestamp?: number) => {
|
||||||
|
if (!isLowErrorVerbosity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (suppressedToolErrorCountRef.current === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (suppressedToolErrorNoteShownRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addItem(
|
||||||
|
{
|
||||||
|
type: MessageType.INFO,
|
||||||
|
text: SUPPRESSED_TOOL_ERRORS_NOTE,
|
||||||
|
},
|
||||||
|
userMessageTimestamp,
|
||||||
|
);
|
||||||
|
suppressedToolErrorNoteShownRef.current = true;
|
||||||
|
},
|
||||||
|
[addItem, isLowErrorVerbosity],
|
||||||
|
);
|
||||||
|
|
||||||
|
const maybeAddLowVerbosityFailureNote = useCallback(
|
||||||
|
(userMessageTimestamp?: number) => {
|
||||||
|
if (!isLowErrorVerbosity || config.getDebugMode()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (lowVerbosityFailureNoteShownRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addItem(
|
||||||
|
{
|
||||||
|
type: MessageType.INFO,
|
||||||
|
text: LOW_VERBOSITY_FAILURE_NOTE,
|
||||||
|
},
|
||||||
|
userMessageTimestamp,
|
||||||
|
);
|
||||||
|
lowVerbosityFailureNoteShownRef.current = true;
|
||||||
|
},
|
||||||
|
[addItem, config, isLowErrorVerbosity],
|
||||||
|
);
|
||||||
|
|
||||||
const cancelOngoingRequest = useCallback(() => {
|
const cancelOngoingRequest = useCallback(() => {
|
||||||
if (
|
if (
|
||||||
streamingState !== StreamingState.Responding &&
|
streamingState !== StreamingState.Responding &&
|
||||||
@@ -908,6 +962,7 @@ export const useGeminiStream = (
|
|||||||
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
addItem(pendingHistoryItemRef.current, userMessageTimestamp);
|
||||||
setPendingHistoryItem(null);
|
setPendingHistoryItem(null);
|
||||||
}
|
}
|
||||||
|
maybeAddSuppressedToolErrorNote(userMessageTimestamp);
|
||||||
addItem(
|
addItem(
|
||||||
{
|
{
|
||||||
type: MessageType.ERROR,
|
type: MessageType.ERROR,
|
||||||
@@ -921,9 +976,18 @@ export const useGeminiStream = (
|
|||||||
},
|
},
|
||||||
userMessageTimestamp,
|
userMessageTimestamp,
|
||||||
);
|
);
|
||||||
|
maybeAddLowVerbosityFailureNote(userMessageTimestamp);
|
||||||
setThought(null); // Reset thought when there's an error
|
setThought(null); // Reset thought when there's an error
|
||||||
},
|
},
|
||||||
[addItem, pendingHistoryItemRef, setPendingHistoryItem, config, setThought],
|
[
|
||||||
|
addItem,
|
||||||
|
pendingHistoryItemRef,
|
||||||
|
setPendingHistoryItem,
|
||||||
|
config,
|
||||||
|
setThought,
|
||||||
|
maybeAddSuppressedToolErrorNote,
|
||||||
|
maybeAddLowVerbosityFailureNote,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleCitationEvent = useCallback(
|
const handleCitationEvent = useCallback(
|
||||||
@@ -1086,6 +1150,7 @@ export const useGeminiStream = (
|
|||||||
},
|
},
|
||||||
userMessageTimestamp,
|
userMessageTimestamp,
|
||||||
);
|
);
|
||||||
|
maybeAddLowVerbosityFailureNote(userMessageTimestamp);
|
||||||
if (contextCleared) {
|
if (contextCleared) {
|
||||||
addItem(
|
addItem(
|
||||||
{
|
{
|
||||||
@@ -1097,7 +1162,13 @@ export const useGeminiStream = (
|
|||||||
}
|
}
|
||||||
setIsResponding(false);
|
setIsResponding(false);
|
||||||
},
|
},
|
||||||
[addItem, pendingHistoryItemRef, setPendingHistoryItem, setIsResponding],
|
[
|
||||||
|
addItem,
|
||||||
|
pendingHistoryItemRef,
|
||||||
|
setPendingHistoryItem,
|
||||||
|
setIsResponding,
|
||||||
|
maybeAddLowVerbosityFailureNote,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleAgentExecutionBlockedEvent = useCallback(
|
const handleAgentExecutionBlockedEvent = useCallback(
|
||||||
@@ -1118,6 +1189,7 @@ export const useGeminiStream = (
|
|||||||
},
|
},
|
||||||
userMessageTimestamp,
|
userMessageTimestamp,
|
||||||
);
|
);
|
||||||
|
maybeAddLowVerbosityFailureNote(userMessageTimestamp);
|
||||||
if (contextCleared) {
|
if (contextCleared) {
|
||||||
addItem(
|
addItem(
|
||||||
{
|
{
|
||||||
@@ -1128,7 +1200,12 @@ export const useGeminiStream = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[addItem, pendingHistoryItemRef, setPendingHistoryItem],
|
[
|
||||||
|
addItem,
|
||||||
|
pendingHistoryItemRef,
|
||||||
|
setPendingHistoryItem,
|
||||||
|
maybeAddLowVerbosityFailureNote,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const processGeminiStreamEvents = useCallback(
|
const processGeminiStreamEvents = useCallback(
|
||||||
@@ -1286,6 +1363,9 @@ export const useGeminiStream = (
|
|||||||
if (!options?.isContinuation) {
|
if (!options?.isContinuation) {
|
||||||
setModelSwitchedFromQuotaError(false);
|
setModelSwitchedFromQuotaError(false);
|
||||||
config.setQuotaErrorOccurred(false);
|
config.setQuotaErrorOccurred(false);
|
||||||
|
suppressedToolErrorCountRef.current = 0;
|
||||||
|
suppressedToolErrorNoteShownRef.current = false;
|
||||||
|
lowVerbosityFailureNoteShownRef.current = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
abortControllerRef.current = new AbortController();
|
abortControllerRef.current = new AbortController();
|
||||||
@@ -1402,6 +1482,7 @@ export const useGeminiStream = (
|
|||||||
) {
|
) {
|
||||||
// Error was handled by validation dialog, don't display again
|
// Error was handled by validation dialog, don't display again
|
||||||
} else if (!isNodeError(error) || error.name !== 'AbortError') {
|
} else if (!isNodeError(error) || error.name !== 'AbortError') {
|
||||||
|
maybeAddSuppressedToolErrorNote(userMessageTimestamp);
|
||||||
addItem(
|
addItem(
|
||||||
{
|
{
|
||||||
type: MessageType.ERROR,
|
type: MessageType.ERROR,
|
||||||
@@ -1415,6 +1496,7 @@ export const useGeminiStream = (
|
|||||||
},
|
},
|
||||||
userMessageTimestamp,
|
userMessageTimestamp,
|
||||||
);
|
);
|
||||||
|
maybeAddLowVerbosityFailureNote(userMessageTimestamp);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (activeQueryIdRef.current === queryId) {
|
if (activeQueryIdRef.current === queryId) {
|
||||||
@@ -1439,6 +1521,8 @@ export const useGeminiStream = (
|
|||||||
startNewPrompt,
|
startNewPrompt,
|
||||||
getPromptCount,
|
getPromptCount,
|
||||||
setThought,
|
setThought,
|
||||||
|
maybeAddSuppressedToolErrorNote,
|
||||||
|
maybeAddLowVerbosityFailureNote,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1587,6 +1671,13 @@ export const useGeminiStream = (
|
|||||||
(t) => !t.request.isClientInitiated,
|
(t) => !t.request.isClientInitiated,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isLowErrorVerbosity) {
|
||||||
|
// Low-mode suppression applies only to model-initiated tool failures.
|
||||||
|
suppressedToolErrorCountRef.current += geminiTools.filter(
|
||||||
|
(tc) => tc.status === CoreToolCallStatus.Error,
|
||||||
|
).length;
|
||||||
|
}
|
||||||
|
|
||||||
if (geminiTools.length === 0) {
|
if (geminiTools.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1597,10 +1688,12 @@ export const useGeminiStream = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (stopExecutionTool && stopExecutionTool.response.error) {
|
if (stopExecutionTool && stopExecutionTool.response.error) {
|
||||||
|
maybeAddSuppressedToolErrorNote();
|
||||||
addItem({
|
addItem({
|
||||||
type: MessageType.INFO,
|
type: MessageType.INFO,
|
||||||
text: `Agent execution stopped: ${stopExecutionTool.response.error.message}`,
|
text: `Agent execution stopped: ${stopExecutionTool.response.error.message}`,
|
||||||
});
|
});
|
||||||
|
maybeAddLowVerbosityFailureNote();
|
||||||
setIsResponding(false);
|
setIsResponding(false);
|
||||||
|
|
||||||
const callIdsToMarkAsSubmitted = geminiTools.map(
|
const callIdsToMarkAsSubmitted = geminiTools.map(
|
||||||
@@ -1706,6 +1799,9 @@ export const useGeminiStream = (
|
|||||||
registerBackgroundShell,
|
registerBackgroundShell,
|
||||||
consumeUserHint,
|
consumeUserHint,
|
||||||
config,
|
config,
|
||||||
|
isLowErrorVerbosity,
|
||||||
|
maybeAddSuppressedToolErrorNote,
|
||||||
|
maybeAddLowVerbosityFailureNote,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ describe('useLoadingIndicator', () => {
|
|||||||
initialShouldShowFocusHint: boolean = false,
|
initialShouldShowFocusHint: boolean = false,
|
||||||
initialRetryStatus: RetryAttemptPayload | null = null,
|
initialRetryStatus: RetryAttemptPayload | null = null,
|
||||||
loadingPhrasesMode: LoadingPhrasesMode = 'all',
|
loadingPhrasesMode: LoadingPhrasesMode = 'all',
|
||||||
|
initialErrorVerbosity: 'low' | 'full' = 'full',
|
||||||
) => {
|
) => {
|
||||||
let hookResult: ReturnType<typeof useLoadingIndicator>;
|
let hookResult: ReturnType<typeof useLoadingIndicator>;
|
||||||
function TestComponent({
|
function TestComponent({
|
||||||
@@ -42,17 +43,20 @@ describe('useLoadingIndicator', () => {
|
|||||||
shouldShowFocusHint,
|
shouldShowFocusHint,
|
||||||
retryStatus,
|
retryStatus,
|
||||||
mode,
|
mode,
|
||||||
|
errorVerbosity,
|
||||||
}: {
|
}: {
|
||||||
streamingState: StreamingState;
|
streamingState: StreamingState;
|
||||||
shouldShowFocusHint?: boolean;
|
shouldShowFocusHint?: boolean;
|
||||||
retryStatus?: RetryAttemptPayload | null;
|
retryStatus?: RetryAttemptPayload | null;
|
||||||
mode?: LoadingPhrasesMode;
|
mode?: LoadingPhrasesMode;
|
||||||
|
errorVerbosity?: 'low' | 'full';
|
||||||
}) {
|
}) {
|
||||||
hookResult = useLoadingIndicator({
|
hookResult = useLoadingIndicator({
|
||||||
streamingState,
|
streamingState,
|
||||||
shouldShowFocusHint: !!shouldShowFocusHint,
|
shouldShowFocusHint: !!shouldShowFocusHint,
|
||||||
retryStatus: retryStatus || null,
|
retryStatus: retryStatus || null,
|
||||||
loadingPhrasesMode: mode,
|
loadingPhrasesMode: mode,
|
||||||
|
errorVerbosity,
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -62,6 +66,7 @@ describe('useLoadingIndicator', () => {
|
|||||||
shouldShowFocusHint={initialShouldShowFocusHint}
|
shouldShowFocusHint={initialShouldShowFocusHint}
|
||||||
retryStatus={initialRetryStatus}
|
retryStatus={initialRetryStatus}
|
||||||
mode={loadingPhrasesMode}
|
mode={loadingPhrasesMode}
|
||||||
|
errorVerbosity={initialErrorVerbosity}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -75,7 +80,15 @@ describe('useLoadingIndicator', () => {
|
|||||||
shouldShowFocusHint?: boolean;
|
shouldShowFocusHint?: boolean;
|
||||||
retryStatus?: RetryAttemptPayload | null;
|
retryStatus?: RetryAttemptPayload | null;
|
||||||
mode?: LoadingPhrasesMode;
|
mode?: LoadingPhrasesMode;
|
||||||
}) => rerender(<TestComponent mode={loadingPhrasesMode} {...newProps} />),
|
errorVerbosity?: 'low' | 'full';
|
||||||
|
}) =>
|
||||||
|
rerender(
|
||||||
|
<TestComponent
|
||||||
|
mode={loadingPhrasesMode}
|
||||||
|
errorVerbosity={initialErrorVerbosity}
|
||||||
|
{...newProps}
|
||||||
|
/>,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -229,6 +242,46 @@ describe('useLoadingIndicator', () => {
|
|||||||
expect(result.current.currentLoadingPhrase).toContain('Attempt 3/3');
|
expect(result.current.currentLoadingPhrase).toContain('Attempt 3/3');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should hide low-verbosity retry status for early retry attempts', () => {
|
||||||
|
const retryStatus = {
|
||||||
|
model: 'gemini-pro',
|
||||||
|
attempt: 1,
|
||||||
|
maxAttempts: 5,
|
||||||
|
delayMs: 1000,
|
||||||
|
};
|
||||||
|
const { result } = renderLoadingIndicatorHook(
|
||||||
|
StreamingState.Responding,
|
||||||
|
false,
|
||||||
|
retryStatus,
|
||||||
|
'all',
|
||||||
|
'low',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.currentLoadingPhrase).not.toBe(
|
||||||
|
"This is taking a bit longer, we're still on it.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a generic retry phrase in low error verbosity mode for later retries', () => {
|
||||||
|
const retryStatus = {
|
||||||
|
model: 'gemini-pro',
|
||||||
|
attempt: 2,
|
||||||
|
maxAttempts: 5,
|
||||||
|
delayMs: 1000,
|
||||||
|
};
|
||||||
|
const { result } = renderLoadingIndicatorHook(
|
||||||
|
StreamingState.Responding,
|
||||||
|
false,
|
||||||
|
retryStatus,
|
||||||
|
'all',
|
||||||
|
'low',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.current.currentLoadingPhrase).toBe(
|
||||||
|
"This is taking a bit longer, we're still on it.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should show no phrases when loadingPhrasesMode is "off"', () => {
|
it('should show no phrases when loadingPhrasesMode is "off"', () => {
|
||||||
const { result } = renderLoadingIndicatorHook(
|
const { result } = renderLoadingIndicatorHook(
|
||||||
StreamingState.Responding,
|
StreamingState.Responding,
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ import {
|
|||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { LoadingPhrasesMode } from '../../config/settings.js';
|
import type { LoadingPhrasesMode } from '../../config/settings.js';
|
||||||
|
|
||||||
|
const LOW_VERBOSITY_RETRY_HINT_ATTEMPT_THRESHOLD = 2;
|
||||||
|
|
||||||
export interface UseLoadingIndicatorProps {
|
export interface UseLoadingIndicatorProps {
|
||||||
streamingState: StreamingState;
|
streamingState: StreamingState;
|
||||||
shouldShowFocusHint: boolean;
|
shouldShowFocusHint: boolean;
|
||||||
retryStatus: RetryAttemptPayload | null;
|
retryStatus: RetryAttemptPayload | null;
|
||||||
loadingPhrasesMode?: LoadingPhrasesMode;
|
loadingPhrasesMode?: LoadingPhrasesMode;
|
||||||
customWittyPhrases?: string[];
|
customWittyPhrases?: string[];
|
||||||
|
errorVerbosity?: 'low' | 'full';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useLoadingIndicator = ({
|
export const useLoadingIndicator = ({
|
||||||
@@ -28,6 +31,7 @@ export const useLoadingIndicator = ({
|
|||||||
retryStatus,
|
retryStatus,
|
||||||
loadingPhrasesMode,
|
loadingPhrasesMode,
|
||||||
customWittyPhrases,
|
customWittyPhrases,
|
||||||
|
errorVerbosity = 'full',
|
||||||
}: UseLoadingIndicatorProps) => {
|
}: UseLoadingIndicatorProps) => {
|
||||||
const [timerResetKey, setTimerResetKey] = useState(0);
|
const [timerResetKey, setTimerResetKey] = useState(0);
|
||||||
const isTimerActive = streamingState === StreamingState.Responding;
|
const isTimerActive = streamingState === StreamingState.Responding;
|
||||||
@@ -70,7 +74,11 @@ export const useLoadingIndicator = ({
|
|||||||
}, [streamingState, elapsedTimeFromTimer]);
|
}, [streamingState, elapsedTimeFromTimer]);
|
||||||
|
|
||||||
const retryPhrase = retryStatus
|
const retryPhrase = retryStatus
|
||||||
? `Trying to reach ${getDisplayString(retryStatus.model)} (Attempt ${retryStatus.attempt + 1}/${retryStatus.maxAttempts})`
|
? errorVerbosity === 'low'
|
||||||
|
? retryStatus.attempt >= LOW_VERBOSITY_RETRY_HINT_ATTEMPT_THRESHOLD
|
||||||
|
? "This is taking a bit longer, we're still on it."
|
||||||
|
: null
|
||||||
|
: `Trying to reach ${getDisplayString(retryStatus.model)} (Attempt ${retryStatus.attempt + 1}/${retryStatus.maxAttempts})`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -161,6 +161,67 @@ describe('useQuotaAndFallback', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should auto-retry transient capacity failures in low verbosity mode', async () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useQuotaAndFallback({
|
||||||
|
config: mockConfig,
|
||||||
|
historyManager: mockHistoryManager,
|
||||||
|
userTier: UserTierId.FREE,
|
||||||
|
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
||||||
|
onShowAuthSelection: mockOnShowAuthSelection,
|
||||||
|
paidTier: null,
|
||||||
|
settings: mockSettings,
|
||||||
|
errorVerbosity: 'low',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = setFallbackHandlerSpy.mock
|
||||||
|
.calls[0][0] as FallbackModelHandler;
|
||||||
|
const intent = await handler(
|
||||||
|
'gemini-pro',
|
||||||
|
'gemini-flash',
|
||||||
|
new RetryableQuotaError('retryable quota', mockGoogleApiError, 5),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(intent).toBe('retry_once');
|
||||||
|
expect(result.current.proQuotaRequest).toBeNull();
|
||||||
|
expect(mockSetModelSwitchedFromQuotaError).not.toHaveBeenCalledWith(true);
|
||||||
|
expect(mockConfig.setQuotaErrorOccurred).not.toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should still prompt for terminal quota in low verbosity mode', async () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useQuotaAndFallback({
|
||||||
|
config: mockConfig,
|
||||||
|
historyManager: mockHistoryManager,
|
||||||
|
userTier: UserTierId.FREE,
|
||||||
|
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
||||||
|
onShowAuthSelection: mockOnShowAuthSelection,
|
||||||
|
paidTier: null,
|
||||||
|
settings: mockSettings,
|
||||||
|
errorVerbosity: 'low',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handler = setFallbackHandlerSpy.mock
|
||||||
|
.calls[0][0] as FallbackModelHandler;
|
||||||
|
let promise: Promise<FallbackIntent | null>;
|
||||||
|
act(() => {
|
||||||
|
promise = handler(
|
||||||
|
'gemini-pro',
|
||||||
|
'gemini-flash',
|
||||||
|
new TerminalQuotaError('pro quota', mockGoogleApiError),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.proQuotaRequest).not.toBeNull();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleProQuotaChoice('retry_later');
|
||||||
|
});
|
||||||
|
await promise!;
|
||||||
|
});
|
||||||
|
|
||||||
describe('Interactive Fallback', () => {
|
describe('Interactive Fallback', () => {
|
||||||
it('should set an interactive request for a terminal quota error', async () => {
|
it('should set an interactive request for a terminal quota error', async () => {
|
||||||
const { result } = renderHook(() =>
|
const { result } = renderHook(() =>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ interface UseQuotaAndFallbackArgs {
|
|||||||
settings: LoadedSettings;
|
settings: LoadedSettings;
|
||||||
setModelSwitchedFromQuotaError: (value: boolean) => void;
|
setModelSwitchedFromQuotaError: (value: boolean) => void;
|
||||||
onShowAuthSelection: () => void;
|
onShowAuthSelection: () => void;
|
||||||
|
errorVerbosity?: 'low' | 'full';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useQuotaAndFallback({
|
export function useQuotaAndFallback({
|
||||||
@@ -52,6 +53,7 @@ export function useQuotaAndFallback({
|
|||||||
settings,
|
settings,
|
||||||
setModelSwitchedFromQuotaError,
|
setModelSwitchedFromQuotaError,
|
||||||
onShowAuthSelection,
|
onShowAuthSelection,
|
||||||
|
errorVerbosity = 'full',
|
||||||
}: UseQuotaAndFallbackArgs) {
|
}: UseQuotaAndFallbackArgs) {
|
||||||
const [proQuotaRequest, setProQuotaRequest] =
|
const [proQuotaRequest, setProQuotaRequest] =
|
||||||
useState<ProQuotaDialogRequest | null>(null);
|
useState<ProQuotaDialogRequest | null>(null);
|
||||||
@@ -165,6 +167,16 @@ export function useQuotaAndFallback({
|
|||||||
message = messageLines.join('\n');
|
message = messageLines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In low verbosity mode, auto-retry transient capacity failures
|
||||||
|
// without interrupting with a dialog.
|
||||||
|
if (
|
||||||
|
errorVerbosity === 'low' &&
|
||||||
|
!isTerminalQuotaError &&
|
||||||
|
!isModelNotFoundError
|
||||||
|
) {
|
||||||
|
return 'retry_once';
|
||||||
|
}
|
||||||
|
|
||||||
setModelSwitchedFromQuotaError(true);
|
setModelSwitchedFromQuotaError(true);
|
||||||
config.setQuotaErrorOccurred(true);
|
config.setQuotaErrorOccurred(true);
|
||||||
|
|
||||||
@@ -200,6 +212,7 @@ export function useQuotaAndFallback({
|
|||||||
initialOverageStrategy,
|
initialOverageStrategy,
|
||||||
setModelSwitchedFromQuotaError,
|
setModelSwitchedFromQuotaError,
|
||||||
onShowAuthSelection,
|
onShowAuthSelection,
|
||||||
|
errorVerbosity,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Set up validation handler for 403 VALIDATION_REQUIRED errors
|
// Set up validation handler for 403 VALIDATION_REQUIRED errors
|
||||||
|
|||||||
@@ -102,6 +102,8 @@ export interface IndividualToolCallDisplay {
|
|||||||
description: string;
|
description: string;
|
||||||
resultDisplay: ToolResultDisplay | undefined;
|
resultDisplay: ToolResultDisplay | undefined;
|
||||||
status: CoreToolCallStatus;
|
status: CoreToolCallStatus;
|
||||||
|
// True when the tool was initiated directly by the user (slash/@/shell flows).
|
||||||
|
isClientInitiated?: boolean;
|
||||||
confirmationDetails: SerializableConfirmationDetails | undefined;
|
confirmationDetails: SerializableConfirmationDetails | undefined;
|
||||||
renderOutputAsMarkdown?: boolean;
|
renderOutputAsMarkdown?: boolean;
|
||||||
ptyId?: number;
|
ptyId?: number;
|
||||||
|
|||||||
@@ -436,6 +436,14 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["tips", "witty", "all", "off"]
|
"enum": ["tips", "witty", "all", "off"]
|
||||||
},
|
},
|
||||||
|
"errorVerbosity": {
|
||||||
|
"title": "Error Verbosity",
|
||||||
|
"description": "Controls whether recoverable errors are hidden (low) or fully shown (full).",
|
||||||
|
"markdownDescription": "Controls whether recoverable errors are hidden (low) or fully shown (full).\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `low`",
|
||||||
|
"default": "low",
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["low", "full"]
|
||||||
|
},
|
||||||
"customWittyPhrases": {
|
"customWittyPhrases": {
|
||||||
"title": "Custom Witty Phrases",
|
"title": "Custom Witty Phrases",
|
||||||
"description": "Custom witty phrases to display during loading. When provided, the CLI cycles through these instead of the defaults.",
|
"description": "Custom witty phrases to display during loading. When provided, the CLI cycles through these instead of the defaults.",
|
||||||
|
|||||||
Reference in New Issue
Block a user