Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 62 additions & 3 deletions packages/cli/src/ui/AppContainer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3465,6 +3465,63 @@ describe('AppContainer State Management', () => {
unmount!();
});

it('resets the hint timer when a new component overflows (overflowingIdsSize increases)', async () => {
let unmount: () => void;
await act(async () => {
const result = renderAppContainer();
unmount = result.unmount;
});
await waitFor(() => expect(capturedUIState).toBeTruthy());

// 1. Trigger first overflow
act(() => {
capturedOverflowActions.addOverflowingId('test-id-1');
});

await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(true);
});

// 2. Advance half the duration
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2);
});
expect(capturedUIState.showIsExpandableHint).toBe(true);

// 3. Trigger second overflow (this should reset the timer)
act(() => {
capturedOverflowActions.addOverflowingId('test-id-2');
});

// Advance by 1ms to allow the OverflowProvider's 0ms batching timeout to fire
// and flush the state update to AppContainer, triggering the reset.
act(() => {
vi.advanceTimersByTime(1);
});

await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(true);
});

// 4. Advance enough that the ORIGINAL timer would have expired
// Subtracting 1ms since we advanced it above to flush the state.
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2 + 100 - 1);
});
// The hint should STILL be visible because the timer reset at step 3
expect(capturedUIState.showIsExpandableHint).toBe(true);

// 5. Advance to the end of the NEW timer
act(() => {
vi.advanceTimersByTime(EXPAND_HINT_DURATION_MS / 2 - 100);
});
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 () => {
let unmount: () => void;
let stdin: ReturnType<typeof renderAppContainer>['stdin'];
Expand Down Expand Up @@ -3606,7 +3663,7 @@ describe('AppContainer State Management', () => {
unmount!();
});

it('does NOT set showIsExpandableHint when overflow occurs in Alternate Buffer Mode', async () => {
it('DOES set showIsExpandableHint when overflow occurs in Alternate Buffer Mode', async () => {
const alternateSettings = mergeSettings({}, {}, {}, {}, true);
const settingsWithAlternateBuffer = {
merged: {
Expand Down Expand Up @@ -3634,8 +3691,10 @@ describe('AppContainer State Management', () => {
capturedOverflowActions.addOverflowingId('test-id');
});

// Should NOT show hint because we are in Alternate Buffer Mode
expect(capturedUIState.showIsExpandableHint).toBe(false);
// Should NOW show hint because we are in Alternate Buffer Mode
await waitFor(() => {
expect(capturedUIState.showIsExpandableHint).toBe(true);
});

unmount!();
});
Expand Down
11 changes: 5 additions & 6 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,19 +283,18 @@ export const AppContainer = (props: AppContainerProps) => {
* 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. We use a stable
* boolean dependency (hasOverflowState) to ensure the timer only resets on
* genuine state transitions, preventing it from infinitely resetting during
* active text streaming.
* 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.
*
* 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.
*/
useEffect(() => {
if (hasOverflowState && !isAlternateBuffer) {
if (hasOverflowState) {
triggerExpandHint(true);
}
}, [hasOverflowState, isAlternateBuffer, triggerExpandHint]);
}, [hasOverflowState, overflowingIdsSize, triggerExpandHint]);

const [defaultBannerText, setDefaultBannerText] = useState('');
const [warningBannerText, setWarningBannerText] = useState('');
Expand Down
6 changes: 1 addition & 5 deletions packages/cli/src/ui/components/FolderTrustDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,9 +311,5 @@ export const FolderTrustDialog: React.FC<FolderTrustDialogProps> = ({
</Box>
);

return isAlternateBuffer ? (
<OverflowProvider>{content}</OverflowProvider>
) : (
content
);
return <OverflowProvider>{content}</OverflowProvider>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we be just returning content rather than always wrapping in an overflow provider?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For FolderTrustDialog.tsx and the ToolConfirmationQueue.tsx, the wrapper helps prevent any overflowing elements w/in these drawers/dialogs from triggering the global hint. At least initially, it allows just their local ShowMoreLine components to render. Without the wrappers we get something like this when they have overflow: https://screenshot.googleplex.com/37BxeV57LD29Mds.png

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok fine for now. please cleanup in a followup so we show the overflow hint with an appropriate message in the main area to keep this simple.

};
6 changes: 4 additions & 2 deletions packages/cli/src/ui/components/ShowMoreLines.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('ShowMoreLines', () => {
},
);

it('renders nothing in STANDARD mode even if overflowing', async () => {
it('renders message in STANDARD mode when overflowing', async () => {
mockUseAlternateBuffer.mockReturnValue(false);
mockUseOverflowState.mockReturnValue({
overflowingIds: new Set(['1']),
Expand All @@ -55,7 +55,9 @@ describe('ShowMoreLines', () => {
<ShowMoreLines constrainHeight={true} />,
);
await waitUntilReady();
expect(lastFrame({ allowEmpty: true })).toBe('');
expect(lastFrame().toLowerCase()).toContain(
'press ctrl+o to show more lines',
);
unmount();
});

Expand Down
3 changes: 0 additions & 3 deletions packages/cli/src/ui/components/ShowMoreLines.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { useOverflowState } from '../contexts/OverflowContext.js';
import { useStreamingContext } from '../contexts/StreamingContext.js';
import { StreamingState } from '../types.js';
import { theme } from '../semantic-colors.js';
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';

interface ShowMoreLinesProps {
constrainHeight: boolean;
Expand All @@ -20,7 +19,6 @@ export const ShowMoreLines = ({
constrainHeight,
isOverflowing: isOverflowingProp,
}: ShowMoreLinesProps) => {
const isAlternateBuffer = useAlternateBuffer();
const overflowState = useOverflowState();
const streamingState = useStreamingContext();

Expand All @@ -29,7 +27,6 @@ export const ShowMoreLines = ({
(overflowState !== undefined && overflowState.overflowingIds.size > 0);

if (
!isAlternateBuffer ||
!isOverflowing ||
!constrainHeight ||
!(
Expand Down
28 changes: 28 additions & 0 deletions packages/cli/src/ui/components/ShowMoreLinesLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,32 @@ describe('ShowMoreLines layout and padding', () => {

unmount();
});

it('renders in Standard mode as well', async () => {
mockUseAlternateBuffer.mockReturnValue(false); // Standard mode

const TestComponent = () => (
<Box flexDirection="column">
<Text>Top</Text>
<ShowMoreLines constrainHeight={true} />
<Text>Bottom</Text>
</Box>
);

const { lastFrame, waitUntilReady, unmount } = render(<TestComponent />);
await waitUntilReady();

const output = lastFrame({ allowEmpty: true });
const lines = output.split('\n');

expect(lines).toEqual([
'Top',
' Press Ctrl+O to show more lines',
'',
'Bottom',
'',
]);

unmount();
});
});
2 changes: 1 addition & 1 deletion packages/cli/src/ui/components/ToastDisplay.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ describe('ToastDisplay', () => {
});
await waitUntilReady();
expect(lastFrame()).toContain(
'Ctrl+O to show more lines of the last response',
'Press Ctrl+O to show more lines of the last response',
);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/ui/components/ToastDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const ToastDisplay: React.FC = () => {
const action = uiState.constrainHeight ? 'show more' : 'collapse';
return (
<Text color={theme.text.accent}>
Ctrl+O to {action} lines of the last response
Press Ctrl+O to {action} lines of the last response
</Text>
);
}
Expand Down
9 changes: 1 addition & 8 deletions packages/cli/src/ui/components/ToolConfirmationQueue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import type { ConfirmingToolState } from '../hooks/useConfirmingTool.js';
import { OverflowProvider } from '../contexts/OverflowContext.js';
import { ShowMoreLines } from './ShowMoreLines.js';
import { StickyHeader } from './StickyHeader.js';
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
import type { SerializableConfirmationDetails } from '@google/gemini-cli-core';
import { useUIActions } from '../contexts/UIActionsContext.js';

Expand Down Expand Up @@ -43,7 +42,6 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
}) => {
const config = useConfig();
const { getPreferredEditor } = useUIActions();
const isAlternateBuffer = useAlternateBuffer();
const {
mainAreaWidth,
terminalHeight,
Expand Down Expand Up @@ -157,10 +155,5 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
</>
);

return isAlternateBuffer ? (
/* Shadow the global provider to maintain isolation in ASB mode. */
<OverflowProvider>{content}</OverflowProvider>
) : (
content
);
return <OverflowProvider>{content}</OverflowProvider>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this defaulting to the alternate buffer version rather than the regular version? these changes should be making alternate buffer mode more like regular mode as far as this goes not vice vesa.

Copy link
Copy Markdown
Contributor Author

@jwhelangoog jwhelangoog Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment

};
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ AppHeader(full)
│ Line 19 █ │
│ Line 20 █ │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
Press Ctrl+O to show more lines
"
`;

Expand All @@ -40,7 +39,6 @@ AppHeader(full)
│ Line 19 █ │
│ Line 20 █ │
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
Press Ctrl+O to show more lines
"
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ exports[`ToolConfirmationQueue > calculates availableContentHeight based on avai
│ 4. No, suggest changes (esc) │
│ │
╰──────────────────────────────────────────────────────────────────────────────╯
Press Ctrl+O to show more lines
"
`;

Expand Down
20 changes: 2 additions & 18 deletions packages/cli/src/ui/components/messages/GeminiMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@
import type React from 'react';
import { Text, Box } from 'ink';
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
import { ShowMoreLines } from '../ShowMoreLines.js';
import { theme } from '../../semantic-colors.js';
import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js';
import { useUIState } from '../../contexts/UIStateContext.js';
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';
import { OverflowProvider } from '../../contexts/OverflowContext.js';

interface GeminiMessageProps {
text: string;
Expand All @@ -31,8 +28,7 @@ export const GeminiMessage: React.FC<GeminiMessageProps> = ({
const prefix = '✦ ';
const prefixWidth = prefix.length;

const isAlternateBuffer = useAlternateBuffer();
const content = (
return (
<Box flexDirection="row">
<Box width={prefixWidth}>
<Text color={theme.text.accent} aria-label={SCREEN_READER_MODEL_PREFIX}>
Expand All @@ -44,26 +40,14 @@ export const GeminiMessage: React.FC<GeminiMessageProps> = ({
text={text}
isPending={isPending}
availableTerminalHeight={
isAlternateBuffer || availableTerminalHeight === undefined
availableTerminalHeight === undefined
? undefined
: Math.max(availableTerminalHeight - 1, 1)
}
terminalWidth={Math.max(terminalWidth - prefixWidth, 0)}
renderMarkdown={renderMarkdown}
/>
<Box>
<ShowMoreLines
constrainHeight={availableTerminalHeight !== undefined}
/>
</Box>
</Box>
</Box>
);

return isAlternateBuffer ? (
/* Shadow the global provider to maintain isolation in ASB mode. */
<OverflowProvider>{content}</OverflowProvider>
) : (
content
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@
import type React from 'react';
import { Box } from 'ink';
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
import { ShowMoreLines } from '../ShowMoreLines.js';
import { useUIState } from '../../contexts/UIStateContext.js';
import { useAlternateBuffer } from '../../hooks/useAlternateBuffer.js';

interface GeminiMessageContentProps {
text: string;
Expand All @@ -31,7 +29,6 @@ export const GeminiMessageContent: React.FC<GeminiMessageContentProps> = ({
terminalWidth,
}) => {
const { renderMarkdown } = useUIState();
const isAlternateBuffer = useAlternateBuffer();
const originalPrefix = '✦ ';
const prefixWidth = originalPrefix.length;

Expand All @@ -41,18 +38,13 @@ export const GeminiMessageContent: React.FC<GeminiMessageContentProps> = ({
text={text}
isPending={isPending}
availableTerminalHeight={
isAlternateBuffer || availableTerminalHeight === undefined
availableTerminalHeight === undefined
? undefined
: Math.max(availableTerminalHeight - 1, 1)
}
terminalWidth={Math.max(terminalWidth - prefixWidth, 0)}
renderMarkdown={renderMarkdown}
/>
<Box>
<ShowMoreLines
constrainHeight={availableTerminalHeight !== undefined}
/>
</Box>
</Box>
);
};
Loading
Loading