mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 22:02:59 -07:00
158 lines
4.6 KiB
TypeScript
158 lines
4.6 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import type React from 'react';
|
|
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
import { Box, Text, ResizeObserver, type DOMElement } from 'ink';
|
|
import { theme } from '../../semantic-colors.js';
|
|
import { useOverflowActions } from '../../contexts/OverflowContext.js';
|
|
import { isNarrowWidth } from '../../utils/isNarrowWidth.js';
|
|
import { Command } from '../../key/keyBindings.js';
|
|
import { formatCommand } from '../../key/keybindingUtils.js';
|
|
|
|
/**
|
|
* Minimum height for the MaxSizedBox component.
|
|
* This ensures there is room for at least one line of content as well as the
|
|
* message that content was truncated.
|
|
*/
|
|
export const MINIMUM_MAX_HEIGHT = 2;
|
|
|
|
export interface MaxSizedBoxProps {
|
|
children?: React.ReactNode;
|
|
maxWidth?: number;
|
|
maxHeight?: number;
|
|
overflowDirection?: 'top' | 'bottom';
|
|
additionalHiddenLinesCount?: number;
|
|
}
|
|
|
|
/**
|
|
* A React component that constrains the size of its children and provides
|
|
* content-aware truncation when the content exceeds the specified `maxHeight`.
|
|
*/
|
|
export const MaxSizedBox: React.FC<MaxSizedBoxProps> = ({
|
|
children,
|
|
maxWidth,
|
|
maxHeight,
|
|
overflowDirection = 'top',
|
|
additionalHiddenLinesCount = 0,
|
|
}) => {
|
|
const id = useId();
|
|
const { addOverflowingId, removeOverflowingId } = useOverflowActions() || {};
|
|
const observerRef = useRef<ResizeObserver | null>(null);
|
|
const [contentHeight, setContentHeight] = useState(0);
|
|
|
|
const onRefChange = useCallback(
|
|
(node: DOMElement | null) => {
|
|
if (observerRef.current) {
|
|
observerRef.current.disconnect();
|
|
observerRef.current = null;
|
|
}
|
|
|
|
if (node && maxHeight !== undefined) {
|
|
const observer = new ResizeObserver((entries) => {
|
|
const entry = entries[0];
|
|
if (entry) {
|
|
setContentHeight(Math.round(entry.contentRect.height));
|
|
}
|
|
});
|
|
observer.observe(node);
|
|
observerRef.current = observer;
|
|
}
|
|
},
|
|
[maxHeight],
|
|
);
|
|
|
|
const effectiveMaxHeight =
|
|
maxHeight !== undefined
|
|
? Math.max(Math.round(maxHeight), MINIMUM_MAX_HEIGHT)
|
|
: undefined;
|
|
|
|
const isOverflowing =
|
|
(effectiveMaxHeight !== undefined && contentHeight > effectiveMaxHeight) ||
|
|
additionalHiddenLinesCount > 0;
|
|
|
|
// If we're overflowing, we need to hide at least 1 line for the message.
|
|
const visibleContentHeight =
|
|
isOverflowing && effectiveMaxHeight !== undefined
|
|
? effectiveMaxHeight - 1
|
|
: effectiveMaxHeight;
|
|
|
|
const hiddenLinesCount =
|
|
visibleContentHeight !== undefined
|
|
? Math.max(0, contentHeight - visibleContentHeight)
|
|
: 0;
|
|
|
|
const totalHiddenLines = hiddenLinesCount + additionalHiddenLinesCount;
|
|
|
|
const isNarrow = maxWidth !== undefined && isNarrowWidth(maxWidth);
|
|
const showMoreKey = formatCommand(Command.SHOW_MORE_LINES);
|
|
|
|
useEffect(() => {
|
|
if (totalHiddenLines > 0) {
|
|
addOverflowingId?.(id);
|
|
} else {
|
|
removeOverflowingId?.(id);
|
|
}
|
|
}, [id, totalHiddenLines, addOverflowingId, removeOverflowingId]);
|
|
|
|
useEffect(
|
|
() => () => {
|
|
removeOverflowingId?.(id);
|
|
},
|
|
[id, removeOverflowingId],
|
|
);
|
|
|
|
if (effectiveMaxHeight === undefined) {
|
|
return (
|
|
<Box flexDirection="column" width={maxWidth}>
|
|
{children}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
const offset =
|
|
hiddenLinesCount > 0 && overflowDirection === 'top' ? -hiddenLinesCount : 0;
|
|
|
|
return (
|
|
<Box
|
|
flexDirection="column"
|
|
width={maxWidth}
|
|
maxHeight={effectiveMaxHeight}
|
|
flexShrink={0}
|
|
>
|
|
{totalHiddenLines > 0 && overflowDirection === 'top' && (
|
|
<Text color={theme.text.secondary} wrap="truncate">
|
|
{isNarrow
|
|
? `... ${totalHiddenLines} hidden (${showMoreKey}) ...`
|
|
: `... first ${totalHiddenLines} line${totalHiddenLines === 1 ? '' : 's'} hidden (${showMoreKey} to show) ...`}
|
|
</Text>
|
|
)}
|
|
<Box
|
|
flexDirection="column"
|
|
overflow="hidden"
|
|
flexGrow={0}
|
|
maxHeight={isOverflowing ? visibleContentHeight : undefined}
|
|
>
|
|
<Box
|
|
flexDirection="column"
|
|
ref={onRefChange}
|
|
flexShrink={0}
|
|
marginTop={offset}
|
|
>
|
|
{children}
|
|
</Box>
|
|
</Box>
|
|
{totalHiddenLines > 0 && overflowDirection === 'bottom' && (
|
|
<Text color={theme.text.secondary} wrap="truncate">
|
|
{isNarrow
|
|
? `... ${totalHiddenLines} hidden (${showMoreKey}) ...`
|
|
: `... last ${totalHiddenLines} line${totalHiddenLines === 1 ? '' : 's'} hidden (${showMoreKey} to show) ...`}
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|