diff --git a/src/renderer/src/components/editor/editor-panel-content-types.ts b/src/renderer/src/components/editor/editor-panel-content-types.ts index 98591ffa8e6..1c137bfb632 100644 --- a/src/renderer/src/components/editor/editor-panel-content-types.ts +++ b/src/renderer/src/components/editor/editor-panel-content-types.ts @@ -29,4 +29,7 @@ export type FileContent = { isStale?: boolean } -export type DiffContent = GitDiffResult +export type DiffContent = GitDiffResult & { + /** Superseded by an external change; still rendered until the lazy reload lands. */ + isStale?: boolean +} diff --git a/src/renderer/src/components/editor/useEditorPanelContentState.ts b/src/renderer/src/components/editor/useEditorPanelContentState.ts index 42ae831264e..4b0eb5cffed 100644 --- a/src/renderer/src/components/editor/useEditorPanelContentState.ts +++ b/src/renderer/src/components/editor/useEditorPanelContentState.ts @@ -187,8 +187,11 @@ export function useEditorPanelContentState({ const next = { ...prev } let changed = false for (const fileId of uniqueIds) { - if (fileId in next) { - delete next[fileId] + const existing = next[fileId] + // Why: keep the last-known diff rendered and swap it when the lazy + // reload lands — dropping it flashes "Loading diff…" on every reveal. + if (existing && existing.isStale !== true) { + next[fileId] = { ...existing, isStale: true } changed = true } } @@ -525,9 +528,13 @@ export function useEditorPanelContentState({ !hasLiveRead(fileReadGenerationRef.current, outstandingFileReadsRef.current, fileId) ) } - const needsDiffRead = (fileId: string): boolean => - !diffContents[fileId] && - !hasLiveRead(diffReadGenerationRef.current, outstandingDiffReadsRef.current, fileId) + const needsDiffRead = (fileId: string): boolean => { + const cached = diffContents[fileId] + return ( + (!cached || cached.isStale === true) && + !hasLiveRead(diffReadGenerationRef.current, outstandingDiffReadsRef.current, fileId) + ) + } useEffect(() => { if (!isVisible) { @@ -643,9 +650,10 @@ export function useEditorPanelContentState({ invalidateDiffContent([current.id]) return } - // Why: the lazy-load effect already fetches on first open; forcing here - // races a duplicate git-diff RPC for the same tab. - if (!diffContentsRef.current[current.id]) { + // Why: the lazy-load effect already fetches on first open and on a retained + // stale entry; forcing here races a duplicate git-diff RPC for the same tab. + const cachedDiff = diffContentsRef.current[current.id] + if (!cachedDiff || cachedDiff.isStale === true) { return } void loadDiffContent(current, { force: true }) diff --git a/src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx b/src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx index bfa26936c15..560bc217d86 100644 --- a/src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx +++ b/src/renderer/src/components/editor/useEditorPanelVisibilityContentState.test.tsx @@ -387,6 +387,40 @@ describe('useEditorPanelContentState visibility', () => { expect(mocks.getRuntimeGitDiff).toHaveBeenCalledTimes(2) }) + it('keeps an invalidated diff on screen until the reveal read lands', async () => { + const file = makeFile('diff-no-flash', { mode: 'diff', diffSource: 'unstaged' }) + const freshDiff = createDeferred() + mocks.getRuntimeGitDiff + .mockResolvedValueOnce(textDiff('old diff')) + .mockReturnValueOnce(freshDiff.promise) + + await act(async () => root.render()) + await vi.waitFor(() => + expect(snapshots.get('main')?.diffContents[file.id]?.modifiedContent).toBe('old diff') + ) + + await act(async () => root.render()) + dispatchExternalChange(file) + // Why: the viewers render "Loading diff…" purely on a missing entry, so a + // retained (stale-marked) entry is what keeps the pane painted. + expect(snapshots.get('main')?.diffContents[file.id]?.modifiedContent).toBe('old diff') + expect(snapshots.get('main')?.diffContents[file.id]?.isStale).toBe(true) + expect(mocks.getRuntimeGitDiff).toHaveBeenCalledOnce() + + await act(async () => root.render()) + await vi.waitFor(() => expect(mocks.getRuntimeGitDiff).toHaveBeenCalledTimes(2)) + expect(snapshots.get('main')?.diffContents[file.id]?.modifiedContent).toBe('old diff') + + await act(async () => { + freshDiff.resolve(textDiff('fresh diff')) + await freshDiff.promise + }) + await vi.waitFor(() => + expect(snapshots.get('main')?.diffContents[file.id]).toEqual(textDiff('fresh diff')) + ) + expect(mocks.getRuntimeGitDiff).toHaveBeenCalledTimes(2) + }) + it('invalidates a hidden Git-status diff without reading until reveal', async () => { const file = makeFile('status', { mode: 'diff', diffSource: 'unstaged' }) const status: GitStatusEntry[] = [ @@ -401,7 +435,7 @@ describe('useEditorPanelContentState visibility', () => { await act(async () => root.render() ) - expect(snapshots.get('main')?.diffContents[file.id]).toBeUndefined() + expect(snapshots.get('main')?.diffContents[file.id]?.isStale).toBe(true) expect(mocks.getRuntimeGitDiff).toHaveBeenCalledOnce() await act(async () => root.render()) @@ -430,7 +464,7 @@ describe('useEditorPanelContentState visibility', () => { dispatchExternalChange(file) await vi.waitFor(() => expect(mocks.readRuntimeFileContent).toHaveBeenCalledTimes(2)) expect(mocks.getRuntimeGitDiff).toHaveBeenCalledOnce() - expect(snapshots.get('main')?.diffContents[file.id]).toBeUndefined() + expect(snapshots.get('main')?.diffContents[file.id]?.isStale).toBe(true) await act(async () => root.render()