diff --git a/bun.lock b/bun.lock index 514abc7ca3..3e526e13a2 100644 --- a/bun.lock +++ b/bun.lock @@ -49,7 +49,7 @@ "@vercel/ai-sdk-openai-websocket-fetch": "^1.0.0", "@xterm/addon-serialize": "^0.14.0", "@xterm/headless": "^6.0.0", - "ai": "^6.0.175", + "ai": "6.0.175", "ai-tokenizer": "^1.0.6", "chalk": "^5.6.2", "comlink": "^4.4.2", diff --git a/package.json b/package.json index 0a3626e0ca..239a125947 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "@vercel/ai-sdk-openai-websocket-fetch": "^1.0.0", "@xterm/addon-serialize": "^0.14.0", "@xterm/headless": "^6.0.0", - "ai": "^6.0.175", + "ai": "6.0.175", "ai-tokenizer": "^1.0.6", "chalk": "^5.6.2", "comlink": "^4.4.2", diff --git a/src/browser/features/RightSidebar/CodeReview/ImmersiveMinimap.test.tsx b/src/browser/features/RightSidebar/CodeReview/ImmersiveMinimap.test.tsx index 515c78f67c..0c24cbb6bb 100644 --- a/src/browser/features/RightSidebar/CodeReview/ImmersiveMinimap.test.tsx +++ b/src/browser/features/RightSidebar/CodeReview/ImmersiveMinimap.test.tsx @@ -97,6 +97,7 @@ describe("ImmersiveMinimap", () => { /> ); + expect(view.getByTestId("immersive-minimap").className).toContain("h-full"); // Synchronous render — no waitFor needed since parseDiffLines runs during render expect(view.getByTestId("immersive-minimap-canvas")).toBeTruthy(); }); @@ -133,6 +134,41 @@ describe("ImmersiveMinimap", () => { expect(parseSpy).toHaveBeenCalledWith(updatedContent); }); + test("redraws from the latest scroll position when parent bumps redraw nonce", () => { + const stableLineCategories: immersiveMinimapMath.LineCategory[] = ["add", "remove", "context"]; + spyOn(immersiveMinimapMath, "parseDiffLines").mockImplementation(() => stableLineCategories); + const getThumbMetricsSpy = spyOn(immersiveMinimapMath, "getThumbMetrics"); + const scrollFixture = createScrollContainerFixture(); + const scrollContainerRef = { current: scrollFixture.element }; + const commentLineIndices = new Set(); + const onSelectLineIndex = mock(() => undefined); + + const renderMinimap = (redrawNonce: number) => ( + + ); + + const view = render(renderMinimap(0)); + const canvas = view.getByTestId("immersive-minimap-canvas") as HTMLCanvasElement; + Object.defineProperty(canvas, "clientWidth", { configurable: true, get: () => 48 }); + Object.defineProperty(canvas, "clientHeight", { configurable: true, get: () => 120 }); + getThumbMetricsSpy.mockClear(); + + scrollFixture.element.scrollTop = 375; + view.rerender(renderMinimap(1)); + + // Parent reveal scroll happens outside the minimap. The nonce is the contract + // that forces a hidden pre-reveal canvas redraw using the new scrollTop even + // when React Compiler keeps parsed line categories stable across renders. + expect(getThumbMetricsSpy).toHaveBeenCalledWith(375, 1000, 250, 120); + }); + test("clicking minimap dispatches the mapped line index", () => { const scrollFixture = createScrollContainerFixture(); const onSelectLineIndex = mock(() => undefined); diff --git a/src/browser/features/RightSidebar/CodeReview/ImmersiveMinimap.tsx b/src/browser/features/RightSidebar/CodeReview/ImmersiveMinimap.tsx index 4ab030466d..0e1af9aa5e 100644 --- a/src/browser/features/RightSidebar/CodeReview/ImmersiveMinimap.tsx +++ b/src/browser/features/RightSidebar/CodeReview/ImmersiveMinimap.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef } from "react"; +import React, { useCallback, useEffect, useLayoutEffect, useRef } from "react"; import { clamp } from "@/common/utils/clamp"; import { @@ -15,6 +15,8 @@ interface ImmersiveMinimapProps { onSelectLineIndex: (lineIndex: number) => void; /** Diff line indices where review comments exist (drawn as yellow indicators) */ commentLineIndices: ReadonlySet; + /** Bump after parent-owned scroll positioning so the canvas reads fresh scrollTop before reveal. */ + redrawNonce?: number; } const DEFAULT_ADD_COLOR = "rgba(34, 197, 94, 0.85)"; @@ -133,9 +135,11 @@ export const ImmersiveMinimap: React.FC = (props) => { } }, [lineCategories, props.activeLineIndex, props.commentLineIndices, props.scrollContainerRef]); - useEffect(() => { + useLayoutEffect(() => { + // Canvas pixels are visible layout state; redraw before paint so hydration + // swaps do not show one frame of the previous minimap/scroll thumb. redrawCanvas(); - }, [redrawCanvas]); + }, [redrawCanvas, props.redrawNonce]); useEffect(() => { const scrollContainer = props.scrollContainerRef.current; @@ -280,7 +284,7 @@ export const ImmersiveMinimap: React.FC = (props) => { } return ( -
+
({ }), })); -import { ImmersiveReviewView } from "./ImmersiveReviewView"; +import { ImmersiveReviewView, shouldPreserveImmersiveContextCursor } from "./ImmersiveReviewView"; function createHunk(overrides: Partial = {}): DiffHunk { return { @@ -114,6 +114,36 @@ function renderImmersiveReview( ); } +function installManualAnimationFrame() { + let nextFrameId = 1; + const callbacks = new Map(); + + const requestAnimationFrameMock = mock((callback: FrameRequestCallback) => { + const frameId = nextFrameId; + nextFrameId += 1; + callbacks.set(frameId, callback); + return frameId; + }) as unknown as typeof globalThis.requestAnimationFrame; + const cancelAnimationFrameMock = mock((frameId: number) => { + callbacks.delete(frameId); + }) as unknown as typeof globalThis.cancelAnimationFrame; + + globalThis.requestAnimationFrame = requestAnimationFrameMock; + globalThis.cancelAnimationFrame = cancelAnimationFrameMock; + globalThis.window.requestAnimationFrame = requestAnimationFrameMock; + globalThis.window.cancelAnimationFrame = cancelAnimationFrameMock; + + return { + flush() { + const pendingCallbacks = Array.from(callbacks.values()); + callbacks.clear(); + for (const callback of pendingCallbacks) { + callback(performance.now()); + } + }, + }; +} + describe("ImmersiveReviewView", () => { let originalWindow: typeof globalThis.window; let originalDocument: typeof globalThis.document; @@ -193,6 +223,116 @@ describe("ImmersiveReviewView", () => { }); }); + test("keeps hydrated full-file context behind the reveal gate until positioned", async () => { + type ExecuteBashValue = Awaited>; + let resolveRead: ((value: ExecuteBashValue) => void) | undefined; + const pendingRead = new Promise((resolve) => { + resolveRead = resolve; + }); + mockApi.workspace.executeBash = mock(() => pendingRead); + const animationFrame = installManualAnimationFrame(); + + const hunk = createHunk({ + newStart: 3, + oldStart: 3, + header: "@@ -3 +3 @@", + content: "-old selected line\n+new selected line", + }); + + const view = renderImmersiveReview({ + fileTree: createFileTree(hunk.filePath), + hunks: [hunk], + allHunks: [hunk], + selectedHunkId: hunk.id, + isTouchImmersive: false, + }); + + await waitFor(() => expect(mockApi.workspace.executeBash).toHaveBeenCalledTimes(1)); + expect(view.getByTestId("immersive-diff-reveal-overlay").textContent ?? "").toContain( + "Loading file" + ); + expect(view.getByTestId("immersive-diff-reveal-stage").className).toContain("invisible"); + + act(() => animationFrame.flush()); + await waitFor(() => expect(view.queryByTestId("immersive-diff-reveal-overlay")).toBeNull()); + expect(view.getByTestId("immersive-diff-reveal-stage").className).not.toContain("invisible"); + + const resolveLoadedContent = resolveRead; + if (!resolveLoadedContent) { + throw new Error("Read promise resolver was not captured"); + } + + act(() => { + resolveLoadedContent({ + success: true, + data: { + success: true, + output: encodeFileReadOutput( + ["context before selected 1", "context before selected 2", "new selected line"].join( + "\n" + ) + ), + exitCode: 0, + }, + }); + }); + + await waitFor(() => + expect(view.getByTestId("immersive-diff-reveal-overlay").textContent ?? "").toContain( + "Preparing diff" + ) + ); + expect(view.container.textContent ?? "").toContain("context before selected 1"); + expect(view.getByTestId("immersive-diff-reveal-stage").className).toContain("invisible"); + expect(view.getByTestId("immersive-minimap-reveal-stage").className).toContain("h-full"); + expect(view.getByTestId("immersive-minimap-reveal-stage").className).toContain("invisible"); + + act(() => animationFrame.flush()); + await waitFor(() => expect(view.queryByTestId("immersive-diff-reveal-overlay")).toBeNull()); + expect(view.getByTestId("immersive-diff-reveal-stage").className).not.toContain("invisible"); + expect(view.getByTestId("immersive-minimap-reveal-stage").className).not.toContain("invisible"); + }); + + test("only preserves context cursors while overlay content is unchanged", () => { + const previousRange = { startIndex: 0, endIndex: 1 }; + + expect( + shouldPreserveImmersiveContextCursor({ + cursorLineIndex: 2, + previousRange, + previousHunkId: "hunk-selected", + currentHunkId: "hunk-selected", + previousOverlayContent: "compact overlay", + currentOverlayContent: "compact overlay", + }) + ).toBe(true); + + // Compact hunk overlays and hydrated full-file overlays use different + // numeric row indices. Do not carry an out-of-hunk compact cursor across + // hydration, or the reveal can replay a stale context/separator row. + expect( + shouldPreserveImmersiveContextCursor({ + cursorLineIndex: 2, + previousRange, + previousHunkId: "hunk-selected", + currentHunkId: "hunk-selected", + previousOverlayContent: "compact overlay", + currentOverlayContent: "hydrated full-file overlay", + }) + ).toBe(false); + + expect( + shouldPreserveImmersiveContextCursor({ + cursorLineIndex: 1, + previousRange, + previousHunkId: "hunk-selected", + currentHunkId: "hunk-selected", + previousOverlayContent: "compact overlay", + currentOverlayContent: "compact overlay", + }) + ).toBe(false); + }); + test("skips full-file reads when the selected hunk starts beyond the render budget", () => { const farHunk = createHunk({ id: "hunk-far", diff --git a/src/browser/features/RightSidebar/CodeReview/ImmersiveReviewView.tsx b/src/browser/features/RightSidebar/CodeReview/ImmersiveReviewView.tsx index b88550ebf1..6c14c6087a 100644 --- a/src/browser/features/RightSidebar/CodeReview/ImmersiveReviewView.tsx +++ b/src/browser/features/RightSidebar/CodeReview/ImmersiveReviewView.tsx @@ -146,6 +146,11 @@ interface ImmersiveOverlayData { hunkLineRanges: Map; } +interface OverlayRevealIdentity { + filePath: string; + content: string; +} + const MAX_FULL_FILE_CONTEXT_LINES = 1500; const MAX_FULL_FILE_CONTEXT_BYTES = 256 * 1024; @@ -364,6 +369,13 @@ function buildOverlayFromHunks(sortedHunks: DiffHunk[]): ImmersiveOverlayData { }; } +function isSameOverlayRevealIdentity( + lhs: OverlayRevealIdentity | null, + rhs: OverlayRevealIdentity | null +): boolean { + return lhs?.filePath === rhs?.filePath && lhs?.content === rhs?.content; +} + function isSelectionInsideRange(selection: SelectedLineRange, range: HunkLineRange): boolean { const start = Math.min(selection.startIndex, selection.endIndex); const end = Math.max(selection.startIndex, selection.endIndex); @@ -376,6 +388,29 @@ function isLineInsideSelection(lineIndex: number, selection: SelectedLineRange): return lineIndex >= start && lineIndex <= end; } +export function shouldPreserveImmersiveContextCursor(input: { + cursorLineIndex: number | null; + previousRange: { startIndex: number; endIndex: number } | null; + previousHunkId: string | null; + currentHunkId: string | null; + previousOverlayContent: string | null; + currentOverlayContent: string; +}): boolean { + if ( + input.cursorLineIndex === null || + !input.previousRange || + input.previousHunkId !== input.currentHunkId || + input.previousOverlayContent !== input.currentOverlayContent + ) { + return false; + } + + return ( + input.cursorLineIndex < input.previousRange.startIndex || + input.cursorLineIndex > input.previousRange.endIndex + ); +} + /** Resolve the hunk that contains a given overlay line index using the lineHunkIds lookup. */ function findHunkAtLine( lineIndex: number, @@ -533,8 +568,7 @@ export const ImmersiveReviewView: React.FC = (props) = return null; }, [selectedHunkId, hunks, allHunks, fileList, isReviewComplete]); - const activeFilePathRef = useRef(null); - activeFilePathRef.current = activeFilePath; + const activeOverlayRevealIdentityRef = useRef(null); const selectedHunkFromAll = useMemo( () => (selectedHunkId ? (allHunks.find((item) => item.id === selectedHunkId) ?? null) : null), @@ -606,10 +640,11 @@ export const ImmersiveReviewView: React.FC = (props) = [activeFilePath, currentFileHunks, props.workspaceId] ); - // Hold diff reveal during file switches until the initial scroll is complete. - // Track the last revealed file instead of setting "pending" from an effect so - // a newly selected file is hidden on its first render rather than for one frame after paint. - const [revealedFilePath, setRevealedFilePath] = useState(null); + // Hold diff reveal until geometry-changing overlay swaps have been positioned. + // This covers file switches and same-file hydration from compact hunk overlays + // into full-file context, so the browser never paints an unanchored diff tree. + const [revealedOverlayIdentity, setRevealedOverlayIdentity] = + useState(null); const revealAnimationFrameRef = useRef(null); // Load full file context only when it is cheap. The hunk overlay remains visible @@ -737,6 +772,32 @@ export const ImmersiveReviewView: React.FC = (props) = return buildOverlayFromHunks(currentFileHunks); }, [resolvedActiveFileContent, currentFileHunks]); + const activeOverlayRevealIdentity = useMemo( + () => (activeFilePath ? { filePath: activeFilePath, content: overlayData.content } : null), + [activeFilePath, overlayData.content] + ); + const isActiveOverlayRevealPending = + activeOverlayRevealIdentity !== null && + !isSameOverlayRevealIdentity(revealedOverlayIdentity, activeOverlayRevealIdentity); + const isActiveFileRevealPending = + activeFilePath !== null && revealedOverlayIdentity?.filePath !== activeFilePath; + const revealLoadingLabel = isActiveFileRevealPending ? "Loading file..." : "Preparing diff..."; + + const scheduleOverlayReveal = useCallback((overlayIdentity: OverlayRevealIdentity) => { + if (revealAnimationFrameRef.current !== null) { + cancelAnimationFrame(revealAnimationFrameRef.current); + } + + revealAnimationFrameRef.current = window.requestAnimationFrame(() => { + setRevealedOverlayIdentity((currentRevealedIdentity) => + isSameOverlayRevealIdentity(activeOverlayRevealIdentityRef.current, overlayIdentity) + ? overlayIdentity + : currentRevealedIdentity + ); + revealAnimationFrameRef.current = null; + }); + }, []); + const selectedHunkRange = useMemo( () => (selectedHunk ? (overlayData.hunkLineRanges.get(selectedHunk.id) ?? null) : null), [selectedHunk, overlayData] @@ -818,6 +879,7 @@ export const ImmersiveReviewView: React.FC = (props) = const [activeLineIndex, setActiveLineIndex] = useState(null); const [selectedLineRange, setSelectedLineRange] = useState(null); const [scrollNonce, setScrollNonce] = useState(0); + const [minimapRedrawNonce, setMinimapRedrawNonce] = useState(0); const [boundaryToast, setBoundaryToast] = useState(null); // Which panel has keyboard focus while in immersive mode. @@ -834,18 +896,20 @@ export const ImmersiveReviewView: React.FC = (props) = revealAnimationFrameRef.current = null; } - if (!activeFilePath) { - setRevealedFilePath(null); + activeOverlayRevealIdentityRef.current = activeOverlayRevealIdentity; + + if (!activeOverlayRevealIdentity) { + setRevealedOverlayIdentity(null); return; } - if (revealedFilePath !== activeFilePath) { - // Keep the splash visible for each file switch until we have scrolled to the target hunk. - // The pending state is derived during render so cross-file hunk iteration never flashes - // the new file at the old scroll position before this effect runs. + if (isActiveOverlayRevealPending) { + // The pending state is derived during render, not set from an effect, so + // file switches and same-file hydration swaps are hidden on their first + // paint until the scroll effect reveals the positioned overlay. hunkJumpRef.current = true; } - }, [activeFilePath, revealedFilePath]); + }, [activeOverlayRevealIdentity, isActiveOverlayRevealPending]); useEffect(() => { return () => { @@ -857,21 +921,20 @@ export const ImmersiveReviewView: React.FC = (props) = const selectedHunkRevealTargetLineIndex = selectedHunkRange?.firstModifiedIndex ?? selectedHunkRange?.startIndex ?? null; - const isActiveFileRevealPending = activeFilePath !== null && revealedFilePath !== activeFilePath; - const revealTargetLineIndex = isActiveFileRevealPending + const revealTargetLineIndex = isActiveOverlayRevealPending ? selectedHunkRevealTargetLineIndex : (activeLineIndex ?? selectedHunkRevealTargetLineIndex); const hasResolvedSelectedHunkForReveal = selectedHunkId !== null && currentFileHunks.some((hunk) => hunk.id === selectedHunkId); useLayoutEffect(() => { - if (!isActiveFileRevealPending) { + if (!isActiveOverlayRevealPending || !activeOverlayRevealIdentity) { return; } // Fail open so the UI cannot get stuck if a file has no hunks. if (currentFileHunks.length === 0) { - setRevealedFilePath(activeFilePath); + setRevealedOverlayIdentity(activeOverlayRevealIdentity); return; } @@ -882,13 +945,13 @@ export const ImmersiveReviewView: React.FC = (props) = // Fail open once selection is stable if we still cannot resolve a reveal target. if (selectedHunkRevealTargetLineIndex === null) { - setRevealedFilePath(activeFilePath); + setRevealedOverlayIdentity(activeOverlayRevealIdentity); } }, [ - activeFilePath, + activeOverlayRevealIdentity, currentFileHunks.length, hasResolvedSelectedHunkForReveal, - isActiveFileRevealPending, + isActiveOverlayRevealPending, selectedHunkRevealTargetLineIndex, ]); @@ -955,6 +1018,7 @@ export const ImmersiveReviewView: React.FC = (props) = const onSelectHunkRef = useRef(onSelectHunk); const allHunksRef = useRef(allHunks); const hunkLineRangesRef = useRef(overlayData.hunkLineRanges); + const previousOverlayContentRef = useRef(null); const previousSelectedHunkIdRef = useRef(null); const previousSelectedHunkRangeRef = useRef(null); const skipScrollUntilCursorSettlesRef = useRef(false); @@ -996,9 +1060,11 @@ export const ImmersiveReviewView: React.FC = (props) = // iteration does not flash the previous cursor/selection for a frame. useLayoutEffect(() => { const resolvedSelectedHunkId = selectedHunk?.id ?? null; + const previousOverlayContent = previousOverlayContentRef.current; const previousSelectedHunkId = previousSelectedHunkIdRef.current; const previousSelectedHunkRange = previousSelectedHunkRangeRef.current; + previousOverlayContentRef.current = overlayData.content; previousSelectedHunkIdRef.current = resolvedSelectedHunkId; previousSelectedHunkRangeRef.current = selectedHunkRange; @@ -1026,23 +1092,20 @@ export const ImmersiveReviewView: React.FC = (props) = } const cursorLineIndex = activeLineIndexRef.current; - const cursorWasInPreviousSelectedHunk = Boolean( - cursorLineIndex !== null && - previousSelectedHunkRange && - cursorLineIndex >= previousSelectedHunkRange.startIndex && - cursorLineIndex <= previousSelectedHunkRange.endIndex - ); - const shouldPreserveContextCursor = Boolean( - cursorLineIndex !== null && - previousSelectedHunkRange && - previousSelectedHunkId === resolvedSelectedHunkId && - !cursorWasInPreviousSelectedHunk - ); + const shouldPreserveContextCursor = shouldPreserveImmersiveContextCursor({ + cursorLineIndex, + previousRange: previousSelectedHunkRange, + previousHunkId: previousSelectedHunkId, + currentHunkId: resolvedSelectedHunkId, + previousOverlayContent, + currentOverlayContent: overlayData.content, + }); if (shouldPreserveContextCursor) { - // Full-file context can arrive after the user has intentionally moved the - // cursor onto an unchanged/context row. Preserve that cursor instead of - // snapping back to the selected hunk just because overlay indices changed. + // Preserve intentional context-row cursor movement only while the rendered + // overlay is unchanged. Compact hunk overlays and hydrated full-file + // overlays use different numeric indices, so carrying a context-row index + // across that geometry swap would reveal at the wrong row. skipScrollUntilCursorSettlesRef.current = false; return; } @@ -1083,6 +1146,7 @@ export const ImmersiveReviewView: React.FC = (props) = return null; }); }, [ + overlayData.content, selectedHunk?.id, selectedHunkRange?.startIndex, selectedHunkRange?.endIndex, @@ -1724,25 +1788,27 @@ export const ImmersiveReviewView: React.FC = (props) = activeLineIndex <= selectedHunkRange.endIndex ); hunkJumpRef.current = Boolean( - isActiveFileRevealPending || + isActiveOverlayRevealPending || skipScrollUntilCursorSettlesRef.current || activeLineIndex === null || !selectedHunkRange || cursorIsInsideSelectedHunk ); - if (!isActiveFileRevealPending) { + if (!isActiveOverlayRevealPending) { return; } } - const lineIndexForScroll = isActiveFileRevealPending ? revealTargetLineIndex : activeLineIndex; + const lineIndexForScroll = isActiveOverlayRevealPending + ? revealTargetLineIndex + : activeLineIndex; if (lineIndexForScroll === null) { return; } if (skipScrollUntilCursorSettlesRef.current) { const cursorHasSettled = - isActiveFileRevealPending || + isActiveOverlayRevealPending || activeLineIndex === null || !selectedHunkRange || (activeLineIndex >= selectedHunkRange.startIndex && @@ -1764,21 +1830,11 @@ export const ImmersiveReviewView: React.FC = (props) = `[data-line-index="${lineIndexForScroll}"]` ); if (!lineElement) { - if (!isActiveFileRevealPending || !activeFilePath || contentChanged) { + if (!isActiveOverlayRevealPending || !activeOverlayRevealIdentity || contentChanged) { return; } - if (revealAnimationFrameRef.current !== null) { - cancelAnimationFrame(revealAnimationFrameRef.current); - } - - const revealFilePath = activeFilePath; - revealAnimationFrameRef.current = window.requestAnimationFrame(() => { - setRevealedFilePath((currentRevealedFilePath) => - activeFilePathRef.current === revealFilePath ? revealFilePath : currentRevealedFilePath - ); - revealAnimationFrameRef.current = null; - }); + scheduleOverlayReveal(activeOverlayRevealIdentity); return; } @@ -1795,27 +1851,24 @@ export const ImmersiveReviewView: React.FC = (props) = hunkJumpRef.current = false; lineElement.scrollIntoView({ behavior: "auto", block }); - if (!isActiveFileRevealPending || !activeFilePath) { + if (!isActiveOverlayRevealPending || !activeOverlayRevealIdentity) { return; } - if (revealAnimationFrameRef.current !== null) { - cancelAnimationFrame(revealAnimationFrameRef.current); + if (!isTouchExperience) { + // The minimap redraws from scrollTop; after a hidden hydration/file-swap + // scroll, force one hidden redraw before the shared reveal gate opens. + setMinimapRedrawNonce((previousNonce) => previousNonce + 1); } - - const revealFilePath = activeFilePath; - revealAnimationFrameRef.current = window.requestAnimationFrame(() => { - setRevealedFilePath((currentRevealedFilePath) => - activeFilePathRef.current === revealFilePath ? revealFilePath : currentRevealedFilePath - ); - revealAnimationFrameRef.current = null; - }); + scheduleOverlayReveal(activeOverlayRevealIdentity); }, [ - activeFilePath, activeLineIndex, - isActiveFileRevealPending, + activeOverlayRevealIdentity, + isActiveOverlayRevealPending, + isTouchExperience, overlayData.content, revealTargetLineIndex, + scheduleOverlayReveal, scrollNonce, selectedHunkRange, ]); @@ -2155,12 +2208,18 @@ export const ImmersiveReviewView: React.FC = (props) =
) : (
- {isActiveFileRevealPending && ( -
- Loading file... + {isActiveOverlayRevealPending && ( +
+ {revealLoadingLabel}
)} -
+
= (props) =
{!isReviewComplete && !isTouchExperience && ( - +
+ +
)} {!isReviewComplete && !isTouchExperience && (