mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
fix(ui): use centralized queue for all transient hints and warnings
This commit migrates several distinct, uncoordinated timers for temporary messages (like raw markdown toggle and overflow hints) into a unified `transientMessageQueue` in `AppContainer.tsx`. This fixes flickering issues caused when the UI rendered is taller than the terminal window in standard mode by adding a `useLegacyNonAlternateBufferMode` hook to pause the message queue when the content overflows. This ensures that the "Show more lines" or "raw markdown mode" hints do not cause continuous re-renders and terminal scrolling. It also removes the old permanent `RawMarkdownIndicator` component entirely, replacing it with a clean, temporary hint when the mode is toggled via keyboard shortcut. Fixes #21824
This commit is contained in:
@@ -646,26 +646,6 @@ describe('Composer', () => {
|
||||
expect(lastFrame()).toMatch(/ShellModeIndic[\s\S]*tor/);
|
||||
});
|
||||
|
||||
it('shows RawMarkdownIndicator when renderMarkdown is false', async () => {
|
||||
const uiState = createMockUIState({
|
||||
renderMarkdown: false,
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).toContain('raw markdown mode');
|
||||
});
|
||||
|
||||
it('does not show RawMarkdownIndicator when renderMarkdown is true', async () => {
|
||||
const uiState = createMockUIState({
|
||||
renderMarkdown: true,
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState);
|
||||
|
||||
expect(lastFrame()).not.toContain('raw markdown mode');
|
||||
});
|
||||
|
||||
it.each([
|
||||
[ApprovalMode.YOLO, 'YOLO'],
|
||||
[ApprovalMode.PLAN, 'plan'],
|
||||
|
||||
@@ -17,7 +17,6 @@ import { ToastDisplay, shouldShowToast } from './ToastDisplay.js';
|
||||
import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
|
||||
import { ShellModeIndicator } from './ShellModeIndicator.js';
|
||||
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
|
||||
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
|
||||
import { ShortcutsHint } from './ShortcutsHint.js';
|
||||
import { ShortcutsHelp } from './ShortcutsHelp.js';
|
||||
import { InputPrompt } from './InputPrompt.js';
|
||||
@@ -114,7 +113,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
suggestionsVisible && suggestionsPosition === 'above';
|
||||
const showApprovalIndicator =
|
||||
!uiState.shellModeActive && !hideUiDetailsForSuggestions;
|
||||
const showRawMarkdownIndicator = !uiState.renderMarkdown;
|
||||
let modeBleedThrough: { text: string; color: string } | null = null;
|
||||
switch (showApprovalModeIndicator) {
|
||||
case ApprovalMode.YOLO:
|
||||
@@ -378,26 +376,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
<ShellModeIndicator />
|
||||
</Box>
|
||||
)}
|
||||
{showRawMarkdownIndicator && (
|
||||
<Box
|
||||
marginLeft={
|
||||
(showApprovalIndicator ||
|
||||
uiState.shellModeActive) &&
|
||||
!isNarrow
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
marginTop={
|
||||
(showApprovalIndicator ||
|
||||
uiState.shellModeActive) &&
|
||||
!isNarrow
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
>
|
||||
<RawMarkdownIndicator />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { render } from '../../test-utils/render.js';
|
||||
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
|
||||
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
|
||||
|
||||
describe('RawMarkdownIndicator', () => {
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
beforeEach(() => vi.stubEnv('FORCE_GENERIC_KEYBINDING_HINTS', ''));
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: originalPlatform,
|
||||
});
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('renders correct key binding for darwin', async () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'darwin',
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<RawMarkdownIndicator />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('raw markdown mode');
|
||||
expect(lastFrame()).toContain('Option+M to toggle');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders correct key binding for other platforms', async () => {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
value: 'linux',
|
||||
});
|
||||
const { lastFrame, waitUntilReady, unmount } = render(
|
||||
<RawMarkdownIndicator />,
|
||||
);
|
||||
await waitUntilReady();
|
||||
expect(lastFrame()).toContain('raw markdown mode');
|
||||
expect(lastFrame()).toContain('Alt+M to toggle');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { formatCommand } from '../utils/keybindingUtils.js';
|
||||
import { Command } from '../../config/keyBindings.js';
|
||||
|
||||
export const RawMarkdownIndicator: React.FC = () => {
|
||||
const modKey = formatCommand(Command.TOGGLE_MARKDOWN);
|
||||
return (
|
||||
<Box>
|
||||
<Text>
|
||||
raw markdown mode
|
||||
<Text color={theme.text.secondary}> ({modKey} to toggle) </Text>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -4,8 +4,11 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useLayoutEffect, type RefObject } from 'react';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { type DOMElement, measureElement } from 'ink';
|
||||
import { useTerminalSize } from './useTerminalSize.js';
|
||||
|
||||
export const isAlternateBufferEnabled = (config: Config): boolean =>
|
||||
config.getUseAlternateBuffer();
|
||||
@@ -15,3 +18,28 @@ export const useAlternateBuffer = (): boolean => {
|
||||
const config = useConfig();
|
||||
return isAlternateBufferEnabled(config);
|
||||
};
|
||||
|
||||
export const useLegacyNonAlternateBufferMode = (
|
||||
rootUiRef: RefObject<DOMElement | null>,
|
||||
): boolean => {
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const { rows: terminalHeight } = useTerminalSize();
|
||||
const [isOverflowing, setIsOverflowing] = useState(false);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (isAlternateBuffer || !rootUiRef.current) {
|
||||
if (isOverflowing) setIsOverflowing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const measurement = measureElement(rootUiRef.current);
|
||||
// If the interactive UI is taller than the terminal height, we have a problem.
|
||||
const currentlyOverflowing = measurement.height >= terminalHeight;
|
||||
|
||||
if (currentlyOverflowing !== isOverflowing) {
|
||||
setIsOverflowing(currentlyOverflowing);
|
||||
}
|
||||
}, [isAlternateBuffer, rootUiRef, terminalHeight, isOverflowing]);
|
||||
|
||||
return !isAlternateBuffer && isOverflowing;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user