import { useState, useEffect, useCallback } from "react"; import { X, FileCode, ChevronLeft, ChevronRight, WrapText, RefreshCw, GitCommit, } from "lucide-react"; import type { MergeDetails, Column } from "@fusion/core"; import { highlightDiff } from "../utils/highlightDiff"; import "./TaskDiffShared.css"; import "./ChangesDiffModal.css"; /** Normalized file entry — re-exported from TaskChangesTab for shared use */ export interface NormalizedFile { path: string; status: "added" | "modified" | "deleted" | "unknown"; additions: number; deletions: number; patch: string; } interface ChangesDiffModalProps { isOpen: boolean; taskId: string; files: NormalizedFile[]; stats: { filesChanged: number; additions: number; deletions: number }; mergeDetails?: MergeDetails; column?: Column; onClose: () => void; onRefresh?: () => void; } function getStatusLabel( status: "added" | "modified" | "deleted" | "unknown" ): string { switch (status) { case "added": return "A"; case "deleted": return "D"; case "modified": return "M"; default: return "?"; } } /** * ChangesDiffModal is a two-panel file browser + diff viewer modal. * * The left panel lists changed files with status badges (A/M/D) and +/- stats. * The right panel displays the syntax-highlighted diff for the selected file. */ export function ChangesDiffModal({ isOpen, taskId, files, stats, mergeDetails, column, onClose, onRefresh, }: ChangesDiffModalProps) { const [selectedIndex, setSelectedIndex] = useState(null); const [wordWrap, setWordWrap] = useState(true); // Auto-select first file when files change useEffect(() => { if (files.length > 0 && selectedIndex === null) { setSelectedIndex(0); } }, [files, selectedIndex]); const navigatePrev = useCallback(() => { setSelectedIndex((prev) => (prev !== null && prev > 0 ? prev - 1 : prev)); }, []); const navigateNext = useCallback(() => { setSelectedIndex((prev) => prev !== null && prev < files.length - 1 ? prev + 1 : prev ); }, []); // Keyboard handler useEffect(() => { if (!isOpen) return; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { onClose(); return; } if (e.key === "ArrowUp" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); navigatePrev(); } if (e.key === "ArrowDown" && (e.metaKey || e.ctrlKey)) { e.preventDefault(); navigateNext(); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [isOpen, onClose, navigatePrev, navigateNext]); if (!isOpen) return null; const selectedFile = selectedIndex !== null ? files[selectedIndex] : null; const isDone = column === "done"; return (
e.stopPropagation()} > {/* Header */}
Changes — {taskId} +{stats.additions}{" "} -{stats.deletions}
{files.length > 0 && (
{selectedIndex !== null ? `${selectedIndex + 1}/${files.length}` : `—/${files.length}`}
)} {onRefresh && ( )}
{/* Body */}
{/* Left panel — file list */}
{/* Commit metadata for done tasks */} {isDone && mergeDetails && (
{mergeDetails.commitSha && (
{mergeDetails.commitSha.slice(0, 7)}
)} {mergeDetails.mergeCommitMessage && (
{mergeDetails.mergeCommitMessage}
)} {mergeDetails.mergedAt && (
Merged {new Date(mergeDetails.mergedAt).toLocaleString()}
)}
)}
{files.map((file, index) => ( ))}
{/* Right panel — diff viewer */}
{selectedFile ? ( <>
{selectedFile.path} +{selectedFile.additions} -{selectedFile.deletions}
{selectedFile.patch ? (
                      {highlightDiff(selectedFile.patch)}
                    
) : (
No diff available for this file.
)} ) : (

Select a file to view its diff

)}
); }