Support ink scrolling final pr (#12567)

This commit is contained in:
Jacob Richman
2025-11-11 07:50:11 -08:00
committed by GitHub
parent 7bb13d1c41
commit cbbf565121
43 changed files with 2498 additions and 1568 deletions
+110 -55
View File
@@ -11,19 +11,23 @@ import { OverflowProvider } from '../contexts/OverflowContext.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useAppContext } from '../contexts/AppContext.js';
import { AppHeader } from './AppHeader.js';
import { useSettings } from '../contexts/SettingsContext.js';
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
import { SCROLL_TO_ITEM_END } from './shared/VirtualizedList.js';
import { ScrollableList } from './shared/ScrollableList.js';
import { useMemo, memo, useCallback } from 'react';
import { MAX_GEMINI_MESSAGE_LINES } from '../constants.js';
const MemoizedHistoryItemDisplay = memo(HistoryItemDisplay);
const MemoizedAppHeader = memo(AppHeader);
// Limit Gemini messages to a very high number of lines to mitigate performance
// issues in the worst case if we somehow get an enormous response from Gemini.
// This threshold is arbitrary but should be high enough to never impact normal
// usage.
const MAX_GEMINI_MESSAGE_LINES = 65536;
export const MainContent = () => {
const { version } = useAppContext();
const uiState = useUIState();
const settings = useSettings();
const useAlternateBuffer = settings.merged.ui?.useAlternateBuffer ?? false;
const isAlternateBuffer = useAlternateBuffer();
const {
pendingHistoryItems,
@@ -32,65 +36,116 @@ export const MainContent = () => {
availableTerminalHeight,
} = uiState;
const historyItems = [
<AppHeader key="app-header" version={version} />,
...uiState.history.map((h) => (
<HistoryItemDisplay
terminalWidth={mainAreaWidth}
availableTerminalHeight={staticAreaMaxItemHeight}
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
key={h.id}
item={h}
isPending={false}
commands={uiState.slashCommands}
/>
)),
];
const historyItems = uiState.history.map((h) => (
<HistoryItemDisplay
terminalWidth={mainAreaWidth}
availableTerminalHeight={staticAreaMaxItemHeight}
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
key={h.id}
item={h}
isPending={false}
commands={uiState.slashCommands}
/>
));
const pendingItems = (
<OverflowProvider>
<Box flexDirection="column" width={mainAreaWidth}>
{pendingHistoryItems.map((item, i) => (
<HistoryItemDisplay
key={i}
availableTerminalHeight={
uiState.constrainHeight ? availableTerminalHeight : undefined
}
terminalWidth={mainAreaWidth}
item={{ ...item, id: 0 }}
isPending={true}
isFocused={!uiState.isEditorDialogOpen}
activeShellPtyId={uiState.activePtyId}
embeddedShellFocused={uiState.embeddedShellFocused}
/>
))}
<ShowMoreLines constrainHeight={uiState.constrainHeight} />
</Box>
</OverflowProvider>
const pendingItems = useMemo(
() => (
<OverflowProvider>
<Box flexDirection="column">
{pendingHistoryItems.map((item, i) => (
<HistoryItemDisplay
key={i}
availableTerminalHeight={
uiState.constrainHeight ? availableTerminalHeight : undefined
}
terminalWidth={mainAreaWidth}
item={{ ...item, id: 0 }}
isPending={true}
isFocused={!uiState.isEditorDialogOpen}
activeShellPtyId={uiState.activePtyId}
embeddedShellFocused={uiState.embeddedShellFocused}
/>
))}
<ShowMoreLines constrainHeight={uiState.constrainHeight} />
</Box>
</OverflowProvider>
),
[
pendingHistoryItems,
uiState.constrainHeight,
availableTerminalHeight,
mainAreaWidth,
uiState.isEditorDialogOpen,
uiState.activePtyId,
uiState.embeddedShellFocused,
],
);
if (useAlternateBuffer) {
// Placeholder alternate buffer implementation using a scrollable box that
// is always scrolled to the bottom. In follow up PRs we will switch this
// to a proper alternate buffer implementation.
const virtualizedData = useMemo(
() => [
{ type: 'header' as const },
...uiState.history.map((item) => ({ type: 'history' as const, item })),
{ type: 'pending' as const },
],
[uiState.history],
);
const renderItem = useCallback(
({ item }: { item: (typeof virtualizedData)[number] }) => {
if (item.type === 'header') {
return <MemoizedAppHeader key="app-header" version={version} />;
} else if (item.type === 'history') {
return (
<MemoizedHistoryItemDisplay
terminalWidth={mainAreaWidth}
availableTerminalHeight={staticAreaMaxItemHeight}
availableTerminalHeightGemini={MAX_GEMINI_MESSAGE_LINES}
key={item.item.id}
item={item.item}
isPending={false}
commands={uiState.slashCommands}
/>
);
} else {
return pendingItems;
}
},
[
version,
mainAreaWidth,
staticAreaMaxItemHeight,
uiState.slashCommands,
pendingItems,
],
);
if (isAlternateBuffer) {
return (
<Box
flexDirection="column"
overflowY="scroll"
scrollTop={Number.MAX_SAFE_INTEGER}
maxHeight={availableTerminalHeight}
>
<Box flexDirection="column" flexShrink={0}>
{historyItems}
{pendingItems}
</Box>
</Box>
<ScrollableList
hasFocus={!uiState.isEditorDialogOpen}
data={virtualizedData}
renderItem={renderItem}
estimatedItemHeight={() => 100}
keyExtractor={(item, _index) => {
if (item.type === 'header') return 'header';
if (item.type === 'history') return item.item.id.toString();
return 'pending';
}}
initialScrollIndex={SCROLL_TO_ITEM_END}
initialScrollOffsetInIndex={SCROLL_TO_ITEM_END}
/>
);
}
return (
<>
<Static key={uiState.historyRemountKey} items={historyItems}>
<Static
key={uiState.historyRemountKey}
items={[
<AppHeader key="app-header" version={version} />,
...historyItems,
]}
>
{(item) => item}
</Static>
{pendingItems}