mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-14 16:10:59 -07:00
feat(cli): truncate shell output in UI history and improve active shell display (#17438)
This commit is contained in:
@@ -117,4 +117,91 @@ describe('<Scrollable />', () => {
|
||||
});
|
||||
expect(capturedEntry.getScrollState().scrollTop).toBe(1);
|
||||
});
|
||||
|
||||
describe('keypress handling', () => {
|
||||
it.each([
|
||||
{
|
||||
name: 'scrolls down when overflow exists and not at bottom',
|
||||
initialScrollTop: 0,
|
||||
scrollHeight: 10,
|
||||
keySequence: '\u001B[1;2B', // Shift+Down
|
||||
expectedScrollTop: 1,
|
||||
},
|
||||
{
|
||||
name: 'scrolls up when overflow exists and not at top',
|
||||
initialScrollTop: 2,
|
||||
scrollHeight: 10,
|
||||
keySequence: '\u001B[1;2A', // Shift+Up
|
||||
expectedScrollTop: 1,
|
||||
},
|
||||
{
|
||||
name: 'does not scroll up when at top (allows event to bubble)',
|
||||
initialScrollTop: 0,
|
||||
scrollHeight: 10,
|
||||
keySequence: '\u001B[1;2A', // Shift+Up
|
||||
expectedScrollTop: 0,
|
||||
},
|
||||
{
|
||||
name: 'does not scroll down when at bottom (allows event to bubble)',
|
||||
initialScrollTop: 5, // maxScroll = 10 - 5 = 5
|
||||
scrollHeight: 10,
|
||||
keySequence: '\u001B[1;2B', // Shift+Down
|
||||
expectedScrollTop: 5,
|
||||
},
|
||||
{
|
||||
name: 'does not scroll when content fits (allows event to bubble)',
|
||||
initialScrollTop: 0,
|
||||
scrollHeight: 5, // Same as innerHeight (5)
|
||||
keySequence: '\u001B[1;2B', // Shift+Down
|
||||
expectedScrollTop: 0,
|
||||
},
|
||||
])(
|
||||
'$name',
|
||||
async ({
|
||||
initialScrollTop,
|
||||
scrollHeight,
|
||||
keySequence,
|
||||
expectedScrollTop,
|
||||
}) => {
|
||||
// Dynamically import ink to mock getScrollHeight
|
||||
const ink = await import('ink');
|
||||
vi.mocked(ink.getScrollHeight).mockReturnValue(scrollHeight);
|
||||
|
||||
let capturedEntry: ScrollProviderModule.ScrollableEntry | undefined;
|
||||
vi.spyOn(ScrollProviderModule, 'useScrollable').mockImplementation(
|
||||
(entry, isActive) => {
|
||||
if (isActive) {
|
||||
capturedEntry = entry as ScrollProviderModule.ScrollableEntry;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const { stdin } = renderWithProviders(
|
||||
<Scrollable hasFocus={true} height={5}>
|
||||
<Text>Content</Text>
|
||||
</Scrollable>,
|
||||
);
|
||||
|
||||
// Ensure initial state using existing scrollBy method
|
||||
act(() => {
|
||||
// Reset to top first, then scroll to desired start position
|
||||
capturedEntry!.scrollBy(-100);
|
||||
if (initialScrollTop > 0) {
|
||||
capturedEntry!.scrollBy(initialScrollTop);
|
||||
}
|
||||
});
|
||||
expect(capturedEntry!.getScrollState().scrollTop).toBe(
|
||||
initialScrollTop,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
stdin.write(keySequence);
|
||||
});
|
||||
|
||||
expect(capturedEntry!.getScrollState().scrollTop).toBe(
|
||||
expectedScrollTop,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useKeypress, type Key } from '../../hooks/useKeypress.js';
|
||||
import { useScrollable } from '../../contexts/ScrollProvider.js';
|
||||
import { useAnimatedScrollbar } from '../../hooks/useAnimatedScrollbar.js';
|
||||
import { useBatchedScroll } from '../../hooks/useBatchedScroll.js';
|
||||
import { keyMatchers, Command } from '../../keyMatchers.js';
|
||||
|
||||
interface ScrollableProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -103,14 +104,38 @@ export const Scrollable: React.FC<ScrollableProps> = ({
|
||||
|
||||
useKeypress(
|
||||
(key: Key) => {
|
||||
if (key.shift) {
|
||||
if (key.name === 'up') {
|
||||
scrollByWithAnimation(-1);
|
||||
const { scrollHeight, innerHeight } = sizeRef.current;
|
||||
const scrollTop = getScrollTop();
|
||||
const maxScroll = Math.max(0, scrollHeight - innerHeight);
|
||||
|
||||
// Only capture scroll-up events if there's room;
|
||||
// otherwise allow events to bubble.
|
||||
if (scrollTop > 0) {
|
||||
if (keyMatchers[Command.PAGE_UP](key)) {
|
||||
scrollByWithAnimation(-innerHeight);
|
||||
return true;
|
||||
}
|
||||
if (key.name === 'down') {
|
||||
scrollByWithAnimation(1);
|
||||
if (keyMatchers[Command.SCROLL_UP](key)) {
|
||||
scrollByWithAnimation(-1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Only capture scroll-down events if there's room;
|
||||
// otherwise allow events to bubble.
|
||||
if (scrollTop < maxScroll) {
|
||||
if (keyMatchers[Command.PAGE_DOWN](key)) {
|
||||
scrollByWithAnimation(innerHeight);
|
||||
return true;
|
||||
}
|
||||
if (keyMatchers[Command.SCROLL_DOWN](key)) {
|
||||
scrollByWithAnimation(1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// bubble keypress
|
||||
return false;
|
||||
},
|
||||
{ isActive: hasFocus },
|
||||
);
|
||||
@@ -137,7 +162,7 @@ export const Scrollable: React.FC<ScrollableProps> = ({
|
||||
[getScrollState, scrollByWithAnimation, hasFocusCallback, flashScrollbar],
|
||||
);
|
||||
|
||||
useScrollable(scrollableEntry, hasFocus && ref.current !== null);
|
||||
useScrollable(scrollableEntry, true);
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
||||
@@ -186,9 +186,11 @@ function ScrollableList<T>(
|
||||
if (keyMatchers[Command.SCROLL_UP](key)) {
|
||||
stopSmoothScroll();
|
||||
scrollByWithAnimation(-1);
|
||||
return true;
|
||||
} else if (keyMatchers[Command.SCROLL_DOWN](key)) {
|
||||
stopSmoothScroll();
|
||||
scrollByWithAnimation(1);
|
||||
return true;
|
||||
} else if (
|
||||
keyMatchers[Command.PAGE_UP](key) ||
|
||||
keyMatchers[Command.PAGE_DOWN](key)
|
||||
@@ -200,11 +202,15 @@ function ScrollableList<T>(
|
||||
: scrollState.scrollTop;
|
||||
const innerHeight = scrollState.innerHeight;
|
||||
smoothScrollTo(current + direction * innerHeight);
|
||||
return true;
|
||||
} else if (keyMatchers[Command.SCROLL_HOME](key)) {
|
||||
smoothScrollTo(0);
|
||||
return true;
|
||||
} else if (keyMatchers[Command.SCROLL_END](key)) {
|
||||
smoothScrollTo(SCROLL_TO_ITEM_END);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
{ isActive: hasFocus },
|
||||
);
|
||||
@@ -229,7 +235,7 @@ function ScrollableList<T>(
|
||||
],
|
||||
);
|
||||
|
||||
useScrollable(scrollableEntry, hasFocus);
|
||||
useScrollable(scrollableEntry, true);
|
||||
|
||||
return (
|
||||
<Box
|
||||
|
||||
Reference in New Issue
Block a user