mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-16 04:50:29 -07:00
fix(cli): hide shortcuts hint while model is thinking or the user has typed a prompt + add debounce to avoid flicker (#19389)
This commit is contained in:
@@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest';
|
import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest';
|
||||||
import { render } from '../../test-utils/render.js';
|
import { render } from '../../test-utils/render.js';
|
||||||
|
import { act, useEffect } from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { Composer } from './Composer.js';
|
import { Composer } from './Composer.js';
|
||||||
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';
|
import { UIStateContext, type UIState } from '../contexts/UIStateContext.js';
|
||||||
import {
|
import {
|
||||||
@@ -34,6 +34,7 @@ import { StreamingState } from '../types.js';
|
|||||||
import { TransientMessageType } from '../../utils/events.js';
|
import { TransientMessageType } from '../../utils/events.js';
|
||||||
import type { LoadedSettings } from '../../config/settings.js';
|
import type { LoadedSettings } from '../../config/settings.js';
|
||||||
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
import type { SessionMetrics } from '../contexts/SessionContext.js';
|
||||||
|
import type { TextBuffer } from './shared/text-buffer.js';
|
||||||
|
|
||||||
const composerTestControls = vi.hoisted(() => ({
|
const composerTestControls = vi.hoisted(() => ({
|
||||||
suggestionsVisible: false,
|
suggestionsVisible: false,
|
||||||
@@ -263,16 +264,26 @@ const renderComposer = async (
|
|||||||
</ConfigContext.Provider>,
|
</ConfigContext.Provider>,
|
||||||
);
|
);
|
||||||
await result.waitUntilReady();
|
await result.waitUntilReady();
|
||||||
|
|
||||||
|
// Wait for shortcuts hint debounce if using fake timers
|
||||||
|
if (vi.isFakeTimers()) {
|
||||||
|
await act(async () => {
|
||||||
|
await vi.advanceTimersByTimeAsync(250);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Composer', () => {
|
describe('Composer', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
composerTestControls.suggestionsVisible = false;
|
composerTestControls.suggestionsVisible = false;
|
||||||
composerTestControls.isAlternateBuffer = false;
|
composerTestControls.isAlternateBuffer = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -809,6 +820,28 @@ describe('Composer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Shortcuts Hint', () => {
|
describe('Shortcuts Hint', () => {
|
||||||
|
it('restores shortcuts hint after 200ms debounce when buffer is empty', async () => {
|
||||||
|
const { lastFrame } = await renderComposer(
|
||||||
|
createMockUIState({
|
||||||
|
buffer: { text: '' } as unknown as TextBuffer,
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrame({ allowEmpty: true })).toContain('ShortcutsHint');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show shortcuts hint immediately when buffer has text', async () => {
|
||||||
|
const uiState = createMockUIState({
|
||||||
|
buffer: { text: 'hello' } as unknown as TextBuffer,
|
||||||
|
cleanUiDetailsVisible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = await renderComposer(uiState);
|
||||||
|
|
||||||
|
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||||
|
});
|
||||||
|
|
||||||
it('hides shortcuts hint when showShortcutsHint setting is false', async () => {
|
it('hides shortcuts hint when showShortcutsHint setting is false', async () => {
|
||||||
const uiState = createMockUIState();
|
const uiState = createMockUIState();
|
||||||
const settings = createMockSettings({
|
const settings = createMockSettings({
|
||||||
@@ -857,6 +890,27 @@ describe('Composer', () => {
|
|||||||
expect(lastFrame()).toContain('ShortcutsHint');
|
expect(lastFrame()).toContain('ShortcutsHint');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides shortcuts hint while loading when full UI details are visible', async () => {
|
||||||
|
const uiState = createMockUIState({
|
||||||
|
cleanUiDetailsVisible: true,
|
||||||
|
streamingState: StreamingState.Responding,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = await renderComposer(uiState);
|
||||||
|
|
||||||
|
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides shortcuts hint when text is typed in buffer', async () => {
|
||||||
|
const uiState = createMockUIState({
|
||||||
|
buffer: { text: 'hello' } as unknown as TextBuffer,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { lastFrame } = await renderComposer(uiState);
|
||||||
|
|
||||||
|
expect(lastFrame()).not.toContain('ShortcutsHint');
|
||||||
|
});
|
||||||
|
|
||||||
it('hides shortcuts hint while loading in minimal mode', async () => {
|
it('hides shortcuts hint while loading in minimal mode', async () => {
|
||||||
const uiState = createMockUIState({
|
const uiState = createMockUIState({
|
||||||
cleanUiDetailsVisible: false,
|
cleanUiDetailsVisible: false,
|
||||||
@@ -930,9 +984,10 @@ describe('Composer', () => {
|
|||||||
streamingState: StreamingState.Idle,
|
streamingState: StreamingState.Idle,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { lastFrame } = await renderComposer(uiState);
|
const { lastFrame, unmount } = await renderComposer(uiState);
|
||||||
|
|
||||||
expect(lastFrame()).toContain('ShortcutsHelp');
|
expect(lastFrame()).toContain('ShortcutsHelp');
|
||||||
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides shortcuts help while streaming', async () => {
|
it('hides shortcuts help while streaming', async () => {
|
||||||
@@ -941,9 +996,10 @@ describe('Composer', () => {
|
|||||||
streamingState: StreamingState.Responding,
|
streamingState: StreamingState.Responding,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { lastFrame } = await renderComposer(uiState);
|
const { lastFrame, unmount } = await renderComposer(uiState);
|
||||||
|
|
||||||
expect(lastFrame()).not.toContain('ShortcutsHelp');
|
expect(lastFrame()).not.toContain('ShortcutsHelp');
|
||||||
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides shortcuts help when action is required', async () => {
|
it('hides shortcuts help when action is required', async () => {
|
||||||
@@ -956,9 +1012,10 @@ describe('Composer', () => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { lastFrame } = await renderComposer(uiState);
|
const { lastFrame, unmount } = await renderComposer(uiState);
|
||||||
|
|
||||||
expect(lastFrame()).not.toContain('ShortcutsHelp');
|
expect(lastFrame()).not.toContain('ShortcutsHelp');
|
||||||
|
unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -151,11 +151,30 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
|||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
const hideShortcutsHintForSuggestions = hideUiDetailsForSuggestions;
|
const hideShortcutsHintForSuggestions = hideUiDetailsForSuggestions;
|
||||||
|
const isModelIdle = uiState.streamingState === StreamingState.Idle;
|
||||||
|
const isBufferEmpty = uiState.buffer.text.length === 0;
|
||||||
|
const canShowShortcutsHint =
|
||||||
|
isModelIdle && isBufferEmpty && !hasPendingActionRequired;
|
||||||
|
const [showShortcutsHintDebounced, setShowShortcutsHintDebounced] =
|
||||||
|
useState(canShowShortcutsHint);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!canShowShortcutsHint) {
|
||||||
|
setShowShortcutsHintDebounced(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
setShowShortcutsHintDebounced(true);
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, [canShowShortcutsHint]);
|
||||||
|
|
||||||
const showShortcutsHint =
|
const showShortcutsHint =
|
||||||
settings.merged.ui.showShortcutsHint &&
|
settings.merged.ui.showShortcutsHint &&
|
||||||
!hideShortcutsHintForSuggestions &&
|
!hideShortcutsHintForSuggestions &&
|
||||||
!hideMinimalModeHintWhileBusy &&
|
showShortcutsHintDebounced;
|
||||||
!hasPendingActionRequired;
|
|
||||||
const showMinimalModeBleedThrough =
|
const showMinimalModeBleedThrough =
|
||||||
!hideUiDetailsForSuggestions && Boolean(minimalModeBleedThrough);
|
!hideUiDetailsForSuggestions && Boolean(minimalModeBleedThrough);
|
||||||
const showMinimalInlineLoading = !showUiDetails && showLoadingIndicator;
|
const showMinimalInlineLoading = !showUiDetails && showLoadingIndicator;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Footer
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Composer > Snapshots > matches snapshot while streaming 1`] = `
|
exports[`Composer > Snapshots > matches snapshot while streaming 1`] = `
|
||||||
" LoadingIndicator: Thinking ShortcutsHint
|
" LoadingIndicator: Thinking
|
||||||
────────────────────────────────────────────────────────────────────────────────────────────────────
|
────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
ApprovalModeIndicator
|
ApprovalModeIndicator
|
||||||
InputPrompt: Type your message or @path/to/file
|
InputPrompt: Type your message or @path/to/file
|
||||||
|
|||||||
Reference in New Issue
Block a user