diff --git a/packages/cli/src/ui/components/MainContent.test.tsx b/packages/cli/src/ui/components/MainContent.test.tsx index 3a9e363d69..5586ad8e59 100644 --- a/packages/cli/src/ui/components/MainContent.test.tsx +++ b/packages/cli/src/ui/components/MainContent.test.tsx @@ -89,6 +89,8 @@ describe('MainContent', () => { historyRemountKey: 0, bannerData: { defaultText: '', warningText: '' }, bannerVisible: false, + copyModeEnabled: false, + terminalWidth: 100, }; beforeEach(() => { @@ -173,6 +175,7 @@ describe('MainContent', () => { vi.mocked(useAlternateBuffer).mockReturnValue(isAlternateBuffer); const ptyId = 123; const uiState = { + ...defaultMockUiState, history: [], pendingHistoryItems: [ { diff --git a/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap index d04486fadf..22cbd276a1 100644 --- a/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap @@ -107,9 +107,9 @@ ShowMoreLines" exports[`MainContent > does not constrain height in alternate buffer mode 1`] = ` "ScrollableList AppHeader -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ > Hello -▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ +▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ✦ Hi there ShowMoreLines " diff --git a/packages/cli/src/ui/components/shared/ScrollableList.test.tsx b/packages/cli/src/ui/components/shared/ScrollableList.test.tsx index b899894cc4..3c0ecb31f5 100644 --- a/packages/cli/src/ui/components/shared/ScrollableList.test.tsx +++ b/packages/cli/src/ui/components/shared/ScrollableList.test.tsx @@ -14,6 +14,12 @@ import { MouseProvider } from '../../contexts/MouseContext.js'; import { describe, it, expect, vi } from 'vitest'; import { waitFor } from '../../../test-utils/async.js'; +vi.mock('../../contexts/UIStateContext.js', () => ({ + useUIState: vi.fn(() => ({ + copyModeEnabled: false, + })), +})); + // Mock useStdout to provide a fixed size for testing vi.mock('ink', async (importOriginal) => { const actual = await importOriginal(); diff --git a/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx b/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx index 88fba88bfb..a0c18a04e3 100644 --- a/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx +++ b/packages/cli/src/ui/components/shared/VirtualizedList.test.tsx @@ -16,6 +16,13 @@ import { useState, } from 'react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { UIState } from '../../contexts/UIStateContext.js'; + +vi.mock('../../contexts/UIStateContext.js', () => ({ + useUIState: vi.fn(() => ({ + copyModeEnabled: false, + })), +})); const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -324,4 +331,39 @@ describe('', () => { expect(ref.current?.getScrollState().scrollTop).toBe(4); }); + + it('renders correctly in copyModeEnabled when scrolled', async () => { + const { useUIState } = await import('../../contexts/UIStateContext.js'); + vi.mocked(useUIState).mockReturnValue({ + copyModeEnabled: true, + } as Partial as UIState); + + const longData = Array.from({ length: 100 }, (_, i) => `Item ${i}`); + // Use copy mode + const { lastFrame } = render( + + ( + + {item} + + )} + keyExtractor={(item) => item} + estimatedItemHeight={() => 1} + initialScrollIndex={50} + /> + , + ); + await act(async () => { + await delay(0); + }); + + // Item 50 should be visible + expect(lastFrame()).toContain('Item 50'); + // And surrounding items + expect(lastFrame()).toContain('Item 59'); + // But far away items should not be (ensures we are actually scrolled) + expect(lastFrame()).not.toContain('Item 0'); + }); }); diff --git a/packages/cli/src/ui/components/shared/VirtualizedList.tsx b/packages/cli/src/ui/components/shared/VirtualizedList.tsx index 66b1244754..98e45a695e 100644 --- a/packages/cli/src/ui/components/shared/VirtualizedList.tsx +++ b/packages/cli/src/ui/components/shared/VirtualizedList.tsx @@ -17,6 +17,7 @@ import { import type React from 'react'; import { theme } from '../../semantic-colors.js'; import { useBatchedScroll } from '../../hooks/useBatchedScroll.js'; +import { useUIState } from '../../contexts/UIStateContext.js'; import { type DOMElement, measureElement, Box } from 'ink'; @@ -78,6 +79,7 @@ function VirtualizedList( initialScrollIndex, initialScrollOffsetInIndex, } = props; + const { copyModeEnabled } = useUIState(); const dataRef = useRef(data); useEffect(() => { dataRef.current = data; @@ -474,16 +476,21 @@ function VirtualizedList( return ( - + {renderedItems} diff --git a/packages/cli/src/ui/layouts/DefaultAppLayout.tsx b/packages/cli/src/ui/layouts/DefaultAppLayout.tsx index 43b00095f3..c703f5102f 100644 --- a/packages/cli/src/ui/layouts/DefaultAppLayout.tsx +++ b/packages/cli/src/ui/layouts/DefaultAppLayout.tsx @@ -31,7 +31,9 @@ export const DefaultAppLayout: React.FC = () => { flexDirection="column" width={uiState.terminalWidth} height={isAlternateBuffer ? terminalHeight : undefined} - paddingBottom={isAlternateBuffer ? 1 : undefined} + paddingBottom={ + isAlternateBuffer && !uiState.copyModeEnabled ? 1 : undefined + } flexShrink={0} flexGrow={0} overflow="hidden"