mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -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/);
|
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([
|
it.each([
|
||||||
[ApprovalMode.YOLO, 'YOLO'],
|
[ApprovalMode.YOLO, 'YOLO'],
|
||||||
[ApprovalMode.PLAN, 'plan'],
|
[ApprovalMode.PLAN, 'plan'],
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import { ToastDisplay, shouldShowToast } from './ToastDisplay.js';
|
|||||||
import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
|
import { ApprovalModeIndicator } from './ApprovalModeIndicator.js';
|
||||||
import { ShellModeIndicator } from './ShellModeIndicator.js';
|
import { ShellModeIndicator } from './ShellModeIndicator.js';
|
||||||
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
|
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
|
||||||
import { RawMarkdownIndicator } from './RawMarkdownIndicator.js';
|
|
||||||
import { ShortcutsHint } from './ShortcutsHint.js';
|
import { ShortcutsHint } from './ShortcutsHint.js';
|
||||||
import { ShortcutsHelp } from './ShortcutsHelp.js';
|
import { ShortcutsHelp } from './ShortcutsHelp.js';
|
||||||
import { InputPrompt } from './InputPrompt.js';
|
import { InputPrompt } from './InputPrompt.js';
|
||||||
@@ -114,7 +113,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
|||||||
suggestionsVisible && suggestionsPosition === 'above';
|
suggestionsVisible && suggestionsPosition === 'above';
|
||||||
const showApprovalIndicator =
|
const showApprovalIndicator =
|
||||||
!uiState.shellModeActive && !hideUiDetailsForSuggestions;
|
!uiState.shellModeActive && !hideUiDetailsForSuggestions;
|
||||||
const showRawMarkdownIndicator = !uiState.renderMarkdown;
|
|
||||||
let modeBleedThrough: { text: string; color: string } | null = null;
|
let modeBleedThrough: { text: string; color: string } | null = null;
|
||||||
switch (showApprovalModeIndicator) {
|
switch (showApprovalModeIndicator) {
|
||||||
case ApprovalMode.YOLO:
|
case ApprovalMode.YOLO:
|
||||||
@@ -378,26 +376,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
|||||||
<ShellModeIndicator />
|
<ShellModeIndicator />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{showRawMarkdownIndicator && (
|
|
||||||
<Box
|
|
||||||
marginLeft={
|
|
||||||
(showApprovalIndicator ||
|
|
||||||
uiState.shellModeActive) &&
|
|
||||||
!isNarrow
|
|
||||||
? 1
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
marginTop={
|
|
||||||
(showApprovalIndicator ||
|
|
||||||
uiState.shellModeActive) &&
|
|
||||||
!isNarrow
|
|
||||||
? 1
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<RawMarkdownIndicator />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</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
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { useState, useLayoutEffect, type RefObject } from 'react';
|
||||||
import { useConfig } from '../contexts/ConfigContext.js';
|
import { useConfig } from '../contexts/ConfigContext.js';
|
||||||
import type { Config } from '@google/gemini-cli-core';
|
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 =>
|
export const isAlternateBufferEnabled = (config: Config): boolean =>
|
||||||
config.getUseAlternateBuffer();
|
config.getUseAlternateBuffer();
|
||||||
@@ -15,3 +18,28 @@ export const useAlternateBuffer = (): boolean => {
|
|||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
return isAlternateBufferEnabled(config);
|
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