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:
jacob314
2026-03-09 22:06:17 -07:00
parent 237864eb63
commit 05fe1bce97
5 changed files with 28 additions and 113 deletions

View File

@@ -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'],

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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>
);
};

View File

@@ -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;
};