import React, { useMemo, useState } from 'react'; import { Icon } from '@/components/icon/Icon'; import { Skeleton } from '@/components/ui/skeleton'; import { useI18n } from '@/lib/i18n'; import { getLanguageFromExtension } from '@/lib/toolHelpers'; import { fileDiffFromPatch } from '@/lib/diff/patchFileDiff'; import type { FileDiffMetadata } from '@pierre/diffs'; import type { ForgeFileChange } from '@/lib/forge/types'; import { PierreDiffViewer } from '@/components/views/PierreDiffViewer'; interface ForgeFilesDiffSectionProps { files: ForgeFileChange[] | null; diff?: string | null; loading?: boolean; error?: string | null; } const CHANGE_DESCRIPTORS: Record = { added: { code: 'A', color: 'var(--status-success)' }, removed: { code: 'D', color: 'var(--status-error)' }, renamed: { code: 'R', color: 'var(--status-info)' }, modified: { code: 'M', color: 'var(--status-warning)' }, }; const DEFAULT_DESCRIPTOR = CHANGE_DESCRIPTORS.modified; const descriptorFor = (status?: string): { code: string; color: string } => { if (!status) return DEFAULT_DESCRIPTOR; const key = status.toLowerCase(); return CHANGE_DESCRIPTORS[key] ?? DEFAULT_DESCRIPTOR; }; /** * Split a combined multi-file diff into per-file sections so a file without * its own `patch` field can still show an inline diff. */ const splitDiffSections = (diff: string): string[] => { if (!diff) return []; const sections: string[] = []; let current: string[] = []; for (const line of diff.split('\n')) { if (/^diff --(git|cc|combined) /.test(line)) { if (current.length > 0) { sections.push(current.join('\n')); current = []; } } current.push(line); } if (current.length > 0) { sections.push(current.join('\n')); } return sections; }; const diffSectionFor = (diff: string | null | undefined, filename: string): string | null => { if (!diff) return null; const needle = ` b/${filename}`; const section = splitDiffSections(diff).find((sectionText) => sectionText.includes(needle)); return section && section.trim() ? section : null; }; /** * File-change list for a pull request. Each row shows the change symbol * (A/M/D/R), filename, and add/delete counts, and expands to an inline diff * rendered by PierreDiffViewer (per-file `patch` when present, else the * matching section sliced from the combined `diff`). Pure presentation. */ export const ForgeFilesDiffSection = React.memo(function ForgeFilesDiffSection({ files, diff, loading, error }) { const { t } = useI18n(); const [openPaths, setOpenPaths] = useState>(new Set()); const toggle = React.useCallback((path: string) => { setOpenPaths((previous) => { const next = new Set(previous); if (next.has(path)) { next.delete(path); } else { next.add(path); } return next; }); }, []); const fileDiffs = useMemo(() => { const map = new Map(); for (const file of files ?? []) { const patch = file.patch?.trim() ? file.patch : diffSectionFor(diff, file.filename); map.set(file.filename, patch && patch.trim() ? fileDiffFromPatch(file.filename, patch) : null); } return map; }, [diff, files]); if (loading) { return (
); } if (error) { return (
{error}
); } if (!files || files.length === 0) { return

{t('forge.files.empty')}

; } return (
    {files.map((file) => { const descriptor = descriptorFor(file.status); const isOpen = openPaths.has(file.filename); const fileDiff = fileDiffs.get(file.filename) ?? null; return (
  • {isOpen ? (
    {fileDiff ? ( ) : (

    {t('forge.files.noDiff')}

    )}
    ) : null}
  • ); })}
); });