feat(cli): add global setting to disable UI spinners (#17234)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Gal Zahavi
2026-01-26 16:06:58 -08:00
committed by GitHub
parent 49c26b4801
commit 00f60ef532
7 changed files with 71 additions and 18 deletions
+1
View File
@@ -60,6 +60,7 @@ they appear in the UI.
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` | | Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` | | Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `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` | | 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` |
| Enable Loading Phrases | `ui.accessibility.enableLoadingPhrases` | Enable loading phrases during operations. | `true` | | Enable Loading Phrases | `ui.accessibility.enableLoadingPhrases` | Enable loading phrases during operations. | `true` |
| 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` |
+4
View File
@@ -261,6 +261,10 @@ their corresponding top-level category object in your `settings.json` file.
- **Default:** `true` - **Default:** `true`
- **Requires restart:** Yes - **Requires restart:** Yes
- **`ui.showSpinner`** (boolean):
- **Description:** Show the spinner during operations.
- **Default:** `true`
- **`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.
@@ -555,6 +555,15 @@ const SETTINGS_SCHEMA = {
'Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled.', 'Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled.',
showInDialog: true, showInDialog: true,
}, },
showSpinner: {
type: 'boolean',
label: 'Show Spinner',
category: 'UI',
requiresRestart: false,
default: true,
description: 'Show the spinner during operations.',
showInDialog: true,
},
customWittyPhrases: { customWittyPhrases: {
type: 'array', type: 'array',
label: 'Custom Witty Phrases', label: 'Custom Witty Phrases',
@@ -4,7 +4,10 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import { render } from '../../test-utils/render.js'; import {
renderWithProviders,
createMockSettings,
} from '../../test-utils/render.js';
import { CliSpinner } from './CliSpinner.js'; import { CliSpinner } from './CliSpinner.js';
import { debugState } from '../debug.js'; import { debugState } from '../debug.js';
import { describe, it, expect, beforeEach } from 'vitest'; import { describe, it, expect, beforeEach } from 'vitest';
@@ -16,9 +19,15 @@ describe('<CliSpinner />', () => {
it('should increment debugNumAnimatedComponents on mount and decrement on unmount', () => { it('should increment debugNumAnimatedComponents on mount and decrement on unmount', () => {
expect(debugState.debugNumAnimatedComponents).toBe(0); expect(debugState.debugNumAnimatedComponents).toBe(0);
const { unmount } = render(<CliSpinner />); const { unmount } = renderWithProviders(<CliSpinner />);
expect(debugState.debugNumAnimatedComponents).toBe(1); expect(debugState.debugNumAnimatedComponents).toBe(1);
unmount(); unmount();
expect(debugState.debugNumAnimatedComponents).toBe(0); expect(debugState.debugNumAnimatedComponents).toBe(0);
}); });
it('should not render when showSpinner is false', () => {
const settings = createMockSettings({ ui: { showSpinner: false } });
const { lastFrame } = renderWithProviders(<CliSpinner />, { settings });
expect(lastFrame()).toBe('');
});
}); });
+16 -5
View File
@@ -7,16 +7,27 @@
import Spinner from 'ink-spinner'; import Spinner from 'ink-spinner';
import { type ComponentProps, useEffect } from 'react'; import { type ComponentProps, useEffect } from 'react';
import { debugState } from '../debug.js'; import { debugState } from '../debug.js';
import { useSettings } from '../contexts/SettingsContext.js';
export type SpinnerProps = ComponentProps<typeof Spinner>; export type SpinnerProps = ComponentProps<typeof Spinner>;
export const CliSpinner = (props: SpinnerProps) => { export const CliSpinner = (props: SpinnerProps) => {
const settings = useSettings();
const shouldShow = settings.merged.ui?.showSpinner !== false;
useEffect(() => { useEffect(() => {
debugState.debugNumAnimatedComponents++; if (shouldShow) {
return () => { debugState.debugNumAnimatedComponents++;
debugState.debugNumAnimatedComponents--; return () => {
}; debugState.debugNumAnimatedComponents--;
}, []); };
}
return undefined;
}, [shouldShow]);
if (!shouldShow) {
return null;
}
return <Spinner {...props} />; return <Spinner {...props} />;
}; };
@@ -4,7 +4,7 @@
* 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 type { CompressionDisplayProps } from './CompressionMessage.js'; import type { CompressionDisplayProps } from './CompressionMessage.js';
import { CompressionMessage } from './CompressionMessage.js'; import { CompressionMessage } from './CompressionMessage.js';
import { CompressionStatus } from '@google/gemini-cli-core'; import { CompressionStatus } from '@google/gemini-cli-core';
@@ -27,7 +27,9 @@ describe('<CompressionMessage />', () => {
describe('pending state', () => { describe('pending state', () => {
it('renders pending message when compression is in progress', () => { it('renders pending message when compression is in progress', () => {
const props = createCompressionProps({ isPending: true }); const props = createCompressionProps({ isPending: true });
const { lastFrame, unmount } = render(<CompressionMessage {...props} />); const { lastFrame, unmount } = renderWithProviders(
<CompressionMessage {...props} />,
);
const output = lastFrame(); const output = lastFrame();
expect(output).toContain('Compressing chat history'); expect(output).toContain('Compressing chat history');
@@ -43,7 +45,9 @@ describe('<CompressionMessage />', () => {
newTokenCount: 50, newTokenCount: 50,
compressionStatus: CompressionStatus.COMPRESSED, compressionStatus: CompressionStatus.COMPRESSED,
}); });
const { lastFrame, unmount } = render(<CompressionMessage {...props} />); const { lastFrame, unmount } = renderWithProviders(
<CompressionMessage {...props} />,
);
const output = lastFrame(); const output = lastFrame();
expect(output).toContain('✦'); expect(output).toContain('✦');
@@ -66,7 +70,7 @@ describe('<CompressionMessage />', () => {
newTokenCount: newTokens, newTokenCount: newTokens,
compressionStatus: CompressionStatus.COMPRESSED, compressionStatus: CompressionStatus.COMPRESSED,
}); });
const { lastFrame, unmount } = render( const { lastFrame, unmount } = renderWithProviders(
<CompressionMessage {...props} />, <CompressionMessage {...props} />,
); );
const output = lastFrame(); const output = lastFrame();
@@ -91,7 +95,9 @@ describe('<CompressionMessage />', () => {
compressionStatus: compressionStatus:
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT, CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
}); });
const { lastFrame, unmount } = render(<CompressionMessage {...props} />); const { lastFrame, unmount } = renderWithProviders(
<CompressionMessage {...props} />,
);
const output = lastFrame(); const output = lastFrame();
expect(output).toContain('✦'); expect(output).toContain('✦');
@@ -109,7 +115,9 @@ describe('<CompressionMessage />', () => {
compressionStatus: compressionStatus:
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT, CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
}); });
const { lastFrame, unmount } = render(<CompressionMessage {...props} />); const { lastFrame, unmount } = renderWithProviders(
<CompressionMessage {...props} />,
);
const output = lastFrame(); const output = lastFrame();
expect(output).toContain( expect(output).toContain(
@@ -146,7 +154,7 @@ describe('<CompressionMessage />', () => {
newTokenCount: newTokens, newTokenCount: newTokens,
compressionStatus: CompressionStatus.COMPRESSED, compressionStatus: CompressionStatus.COMPRESSED,
}); });
const { lastFrame, unmount } = render( const { lastFrame, unmount } = renderWithProviders(
<CompressionMessage {...props} />, <CompressionMessage {...props} />,
); );
const output = lastFrame(); const output = lastFrame();
@@ -171,7 +179,7 @@ describe('<CompressionMessage />', () => {
compressionStatus: compressionStatus:
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT, CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
}); });
const { lastFrame, unmount } = render( const { lastFrame, unmount } = renderWithProviders(
<CompressionMessage {...props} />, <CompressionMessage {...props} />,
); );
const output = lastFrame(); const output = lastFrame();
@@ -199,7 +207,7 @@ describe('<CompressionMessage />', () => {
compressionStatus: compressionStatus:
CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT, CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,
}); });
const { lastFrame, unmount } = render( const { lastFrame, unmount } = renderWithProviders(
<CompressionMessage {...props} />, <CompressionMessage {...props} />,
); );
const output = lastFrame(); const output = lastFrame();
@@ -218,7 +226,9 @@ describe('<CompressionMessage />', () => {
isPending: false, isPending: false,
compressionStatus: CompressionStatus.COMPRESSION_FAILED_EMPTY_SUMMARY, compressionStatus: CompressionStatus.COMPRESSION_FAILED_EMPTY_SUMMARY,
}); });
const { lastFrame, unmount } = render(<CompressionMessage {...props} />); const { lastFrame, unmount } = renderWithProviders(
<CompressionMessage {...props} />,
);
const output = lastFrame(); const output = lastFrame();
expect(output).toContain('✦'); expect(output).toContain('✦');
@@ -234,7 +244,9 @@ describe('<CompressionMessage />', () => {
compressionStatus: compressionStatus:
CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR, CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR,
}); });
const { lastFrame, unmount } = render(<CompressionMessage {...props} />); const { lastFrame, unmount } = renderWithProviders(
<CompressionMessage {...props} />,
);
const output = lastFrame(); const output = lastFrame();
expect(output).toContain( expect(output).toContain(
+7
View File
@@ -323,6 +323,13 @@
"default": true, "default": true,
"type": "boolean" "type": "boolean"
}, },
"showSpinner": {
"title": "Show Spinner",
"description": "Show the spinner during operations.",
"markdownDescription": "Show the spinner during operations.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"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.",