import React, { useCallback, useEffect, useRef } from 'react'; import { CodeMirrorDiffView } from './CodeMirrorDiffView'; import { DiffErrorBoundary } from './DiffErrorBoundary'; import { FileSectionPlaceholder } from './FileSectionPlaceholder'; import { getResolvedReviewModifiedContent, hasReviewSnippetText, isReviewFileMissingOnDisk, isReviewTextContentUnavailable, shouldRenderCurrentDiskContextPreview, } from './reviewContentPreview'; import { ReviewDiffContent } from './ReviewDiffContent'; import { shouldRenderCodeMirrorReviewDiff, shouldRenderSnippetReviewPreview, } from './reviewDiffSafety'; import type { EditorView } from '@codemirror/view'; import type { FileChangeWithContent } from '@shared/types'; import type { EditorSelectionInfo } from '@shared/types/editor'; import type { FileChangeSummary } from '@shared/types/review'; interface FileSectionDiffProps { file: FileChangeSummary; fileContent: FileChangeWithContent | null; isLoading: boolean; collapseUnchanged: boolean; onHunkAccepted: (filePath: string, hunkIndex: number) => void; onHunkRejected: (filePath: string, hunkIndex: number) => void; onFullyViewed: (filePath: string) => void; onContentChanged: (filePath: string, content: string) => void; onEditorViewReady: (filePath: string, view: EditorView | null) => void; discardCounter: number; autoViewed: boolean; isViewed: boolean; onSelectionChange?: (info: EditorSelectionInfo | null) => void; globalHunkOffset?: number; totalReviewHunks?: number; } export const FileSectionDiff = ({ file, fileContent, isLoading, collapseUnchanged, onHunkAccepted, onHunkRejected, onFullyViewed, onContentChanged, onEditorViewReady, discardCounter, autoViewed, isViewed, onSelectionChange, globalHunkOffset = 0, totalReviewHunks, }: FileSectionDiffProps): React.ReactElement => { const localEditorViewRef = useRef(null); const sentinelRef = useRef(null); const hasSnippetText = hasReviewSnippetText(file); const canRenderSnippetPreview = hasSnippetText && shouldRenderSnippetReviewPreview(file.snippets); // Notify parent whenever CodeMirrorDiffView creates or destroys its EditorView. // This fires on every editor lifecycle event: initial mount, key-change remount, // and internal recreation (e.g. when `modified` prop changes after Save). const handleViewChange = useCallback( (view: EditorView | null) => { localEditorViewRef.current = view; onEditorViewReady(file.filePath, view); }, [file.filePath, onEditorViewReady] ); // Auto-viewed sentinel observer useEffect(() => { if (!sentinelRef.current || !autoViewed || isViewed) return; const observer = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) { onFullyViewed(file.filePath); } } }, { threshold: 0.85 } ); observer.observe(sentinelRef.current); return () => observer.disconnect(); }, [autoViewed, isViewed, file.filePath, onFullyViewed]); // Loading state if (isLoading) { if (!hasSnippetText) { return ; } return (
{canRenderSnippetPreview ? ( ) : ( )}
); } // Resolve modified content: prefer full content, fall back to write-type snippet // Only write-new/write-update snippets contain the full file - edit snippets are partial const resolvedModified = getResolvedReviewModifiedContent(file, fileContent); const resolvedOriginal = fileContent?.originalFullContent ?? null; const isMissingOnDisk = isReviewFileMissingOnDisk(fileContent); const isContentUnavailable = isReviewTextContentUnavailable(file, fileContent); const hasLedgerManualAction = file.snippets.some( (snippet) => !!snippet.ledger && (snippet.ledger.relation?.kind === 'rename' || (!!snippet.ledger.beforeState?.unavailableReason && snippet.ledger.originalFullContent == null) || (!!snippet.ledger.afterState?.unavailableReason && snippet.ledger.modifiedFullContent == null)) ); // Show CodeMirror only when we have a trustworthy original baseline: // - new files: original is legitimately empty // - otherwise: original must be known (non-null). If original is unknown, do not // pretend it's empty; fall back to snippet-level diff. const canRenderCodeMirror = resolvedModified !== null && (file.isNewFile || resolvedOriginal !== null); const originalForDiff = file.isNewFile ? '' : (resolvedOriginal ?? ''); const canRenderCodeMirrorSafely = canRenderCodeMirror && shouldRenderCodeMirrorReviewDiff(originalForDiff, resolvedModified ?? ''); const canRenderCurrentDiskContext = resolvedModified !== null && shouldRenderCurrentDiskContextPreview(file, fileContent) && shouldRenderCodeMirrorReviewDiff(resolvedModified, resolvedModified); const currentDiskContextContent = canRenderCurrentDiskContext ? resolvedModified : null; if (!canRenderCodeMirrorSafely) { return (
0 ? 'This file change was captured as metadata only; no text diff data is available.' : 'No text diff data is available for this file.' } /> {canRenderSnippetPreview ? ( ) : currentDiskContextContent != null ? ( onHunkAccepted(file.filePath, idx)} onHunkRejected={(idx) => onHunkRejected(file.filePath, idx)} onContentChanged={(content) => onContentChanged(file.filePath, content)} editorViewRef={localEditorViewRef} onViewChange={handleViewChange} onSelectionChange={ onSelectionChange ? (info) => onSelectionChange(info ? { ...info, filePath: file.filePath } : null) : undefined } globalHunkOffset={globalHunkOffset} totalReviewHunks={totalReviewHunks} /> ) : null}
); } return (
{isMissingOnDisk && (
File is missing on disk. This diff may be only a preview from agent logs. Use{' '} Restore to create the file on disk.
)} onHunkAccepted(file.filePath, idx)} onHunkRejected={(idx) => onHunkRejected(file.filePath, idx)} onContentChanged={(content) => onContentChanged(file.filePath, content)} editorViewRef={localEditorViewRef} onViewChange={handleViewChange} onSelectionChange={ onSelectionChange ? (info) => onSelectionChange(info ? { ...info, filePath: file.filePath } : null) : undefined } globalHunkOffset={globalHunkOffset} totalReviewHunks={totalReviewHunks} />
); }; const OversizedDiffNotice = ({ message }: { message: string }): React.ReactElement => { return (
{message}
); };