mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-21 16:57:08 -07:00
Speculative changes to address react bug with overflowing state
This commit is contained in:
@@ -3290,6 +3290,85 @@ describe('AppContainer State Management', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not reset the hint timer when overflowingIdsSize decreases', async () => {
|
||||
const { unmount } = await act(async () => renderAppContainer());
|
||||
await waitFor(() => expect(capturedOverflowActions).toBeTruthy());
|
||||
|
||||
act(() => {
|
||||
capturedOverflowActions.addOverflowingId('test-id-1');
|
||||
capturedOverflowActions.addOverflowingId('test-id-2');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
|
||||
});
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
|
||||
act(() => {
|
||||
capturedOverflowActions.removeOverflowingId('test-id-2');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(false);
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not auto-reset the hint timer for new overflow while expanded', async () => {
|
||||
const { stdin, unmount } = await act(async () => renderAppContainer());
|
||||
await waitFor(() => expect(capturedOverflowActions).toBeTruthy());
|
||||
|
||||
act(() => {
|
||||
capturedOverflowActions.addOverflowingId('test-id-1');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
stdin.write('\x0f'); // Ctrl+O
|
||||
});
|
||||
|
||||
expect(capturedUIState.constrainHeight).toBe(false);
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS - 1000);
|
||||
});
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(true);
|
||||
|
||||
act(() => {
|
||||
capturedOverflowActions.addOverflowingId('test-id-2');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.showIsExpandableHint).toBe(false);
|
||||
});
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('toggles expansion state and resets the hint timer when Ctrl+O is pressed in Standard Mode', async () => {
|
||||
const { stdin, unmount } = await act(async () => renderAppContainer());
|
||||
await waitFor(() => expect(capturedOverflowActions).toBeTruthy());
|
||||
|
||||
@@ -187,6 +187,7 @@ import {
|
||||
isToolAwaitingConfirmation,
|
||||
getAllToolCalls,
|
||||
} from './utils/historyUtils.js';
|
||||
import { shouldAutoTriggerExpandHint } from './utils/expandHint.js';
|
||||
|
||||
interface AppContainerProps {
|
||||
config: Config;
|
||||
@@ -329,24 +330,35 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
const showIsExpandableHint = Boolean(expandHintTrigger);
|
||||
const overflowState = useOverflowState();
|
||||
const overflowingIdsSize = overflowState?.overflowingIds.size ?? 0;
|
||||
const hasOverflowState = overflowingIdsSize > 0 || !constrainHeight;
|
||||
const previousOverflowingIdsSizeRef = useRef(0);
|
||||
|
||||
/**
|
||||
* Manages the visibility and x-second timer for the expansion hint.
|
||||
*
|
||||
* This effect triggers the timer countdown whenever an overflow is detected
|
||||
* or the user manually toggles the expansion state with Ctrl+O.
|
||||
* By depending on overflowingIdsSize, the timer resets when *new* views
|
||||
* overflow, but avoids infinitely resetting during single-view streaming.
|
||||
* while the response is actually constrained. The Ctrl+O handler still
|
||||
* refreshes the hint manually when the user toggles expansion.
|
||||
*
|
||||
* In alternate buffer mode, we don't trigger the hint automatically on overflow
|
||||
* to avoid noise, but the user can still trigger it manually with Ctrl+O.
|
||||
* We only auto-refresh when the number of overflowing regions grows. That
|
||||
* keeps the "show more" hint responsive for newly truncated content without
|
||||
* retriggering on layout churn, overflow shrinkage, or while the content is
|
||||
* already expanded.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (hasOverflowState) {
|
||||
const previousOverflowingIdsSize = previousOverflowingIdsSizeRef.current;
|
||||
|
||||
if (
|
||||
shouldAutoTriggerExpandHint({
|
||||
constrainHeight,
|
||||
overflowingIdsSize,
|
||||
previousOverflowingIdsSize,
|
||||
})
|
||||
) {
|
||||
triggerExpandHint(true);
|
||||
}
|
||||
}, [hasOverflowState, overflowingIdsSize, triggerExpandHint]);
|
||||
|
||||
previousOverflowingIdsSizeRef.current = overflowingIdsSize;
|
||||
}, [constrainHeight, overflowingIdsSize, triggerExpandHint]);
|
||||
|
||||
const [defaultBannerText, setDefaultBannerText] = useState('');
|
||||
const [warningBannerText, setWarningBannerText] = useState('');
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { act } from 'react';
|
||||
import { renderHook } from '../../test-utils/render.js';
|
||||
import { useTimedMessage } from './useTimedMessage.js';
|
||||
|
||||
describe('useTimedMessage', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('resets the timeout when the same message is retriggered', async () => {
|
||||
const { result, unmount } = await renderHook(() => useTimedMessage(1000));
|
||||
|
||||
act(() => {
|
||||
result.current[1]('hint');
|
||||
});
|
||||
expect(result.current[0]).toBe('hint');
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current[1]('hint');
|
||||
});
|
||||
expect(result.current[0]).toBe('hint');
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
expect(result.current[0]).toBe('hint');
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
expect(result.current[0]).toBeNull();
|
||||
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('clears the message immediately when asked to hide it', async () => {
|
||||
const { result, unmount } = await renderHook(() => useTimedMessage(1000));
|
||||
|
||||
act(() => {
|
||||
result.current[1]('hint');
|
||||
});
|
||||
expect(result.current[0]).toBe('hint');
|
||||
|
||||
act(() => {
|
||||
result.current[1](null);
|
||||
});
|
||||
expect(result.current[0]).toBeNull();
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
expect(result.current[0]).toBeNull();
|
||||
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -12,19 +12,29 @@ import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
*/
|
||||
export function useTimedMessage<T>(durationMs: number) {
|
||||
const [message, setMessage] = useState<T | null>(null);
|
||||
const messageRef = useRef<T | null>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const showMessage = useCallback(
|
||||
(msg: T | null) => {
|
||||
setMessage(msg);
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
if (msg !== null) {
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setMessage(null);
|
||||
timeoutRef.current = null;
|
||||
messageRef.current = null;
|
||||
setMessage((prev) => (prev === null ? prev : null));
|
||||
}, durationMs);
|
||||
}
|
||||
|
||||
if (Object.is(messageRef.current, msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
messageRef.current = msg;
|
||||
setMessage(msg);
|
||||
},
|
||||
[durationMs],
|
||||
);
|
||||
@@ -33,6 +43,7 @@ export function useTimedMessage<T>(durationMs: number) {
|
||||
() => () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { shouldAutoTriggerExpandHint } from './expandHint.js';
|
||||
|
||||
describe('shouldAutoTriggerExpandHint', () => {
|
||||
it('returns true when constrained content gains a new overflowing region', () => {
|
||||
expect(
|
||||
shouldAutoTriggerExpandHint({
|
||||
constrainHeight: true,
|
||||
overflowingIdsSize: 2,
|
||||
previousOverflowingIdsSize: 1,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when overflowingIdsSize decreases', () => {
|
||||
expect(
|
||||
shouldAutoTriggerExpandHint({
|
||||
constrainHeight: true,
|
||||
overflowingIdsSize: 1,
|
||||
previousOverflowingIdsSize: 2,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when overflowingIdsSize is unchanged', () => {
|
||||
expect(
|
||||
shouldAutoTriggerExpandHint({
|
||||
constrainHeight: true,
|
||||
overflowingIdsSize: 1,
|
||||
previousOverflowingIdsSize: 1,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false while content is already expanded', () => {
|
||||
expect(
|
||||
shouldAutoTriggerExpandHint({
|
||||
constrainHeight: false,
|
||||
overflowingIdsSize: 2,
|
||||
previousOverflowingIdsSize: 1,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
interface ExpandHintAutoTriggerParams {
|
||||
constrainHeight: boolean;
|
||||
overflowingIdsSize: number;
|
||||
previousOverflowingIdsSize: number;
|
||||
}
|
||||
|
||||
export function shouldAutoTriggerExpandHint({
|
||||
constrainHeight,
|
||||
overflowingIdsSize,
|
||||
previousOverflowingIdsSize,
|
||||
}: ExpandHintAutoTriggerParams): boolean {
|
||||
return constrainHeight && overflowingIdsSize > previousOverflowingIdsSize;
|
||||
}
|
||||
Reference in New Issue
Block a user