import { useEffect, useMemo, useRef, useState, useCallback } from "react"; import { FileEdit, FileMinus, FilePlus, FileSymlink, FolderGit2, ArrowLeft, X, } from "lucide-react"; import { useChangedFiles } from "../hooks/useChangedFiles"; import { highlightDiff } from "../utils/highlightDiff"; import type { TaskFileDiff } from "../api"; const MOBILE_BREAKPOINT = 768; interface ChangedFilesModalProps { taskId: string; worktree: string | undefined; column: string; projectId?: string; isOpen: boolean; onClose: () => void; } function getStatusLabel(status: TaskFileDiff["status"]): string { switch (status) { case "added": return "A"; case "deleted": return "D"; case "renamed": return "R"; default: return "M"; } } function getStatusIcon(status: TaskFileDiff["status"]) { switch (status) { case "added": return ; case "deleted": return ; case "renamed": return ; default: return ; } } function getDiffStat(diff: string): string { const lines = diff.split("\n"); const statLines = lines.filter( (line) => line.startsWith("diff --git") || line.startsWith("index ") || line.startsWith("--- ") || line.startsWith("+++ "), ); return statLines.join("\n").trim(); } export function ChangedFilesModal({ taskId, worktree, column, projectId, isOpen, onClose, }: ChangedFilesModalProps) { const { files, loading, error, selectedFile, setSelectedFile, resetSelection } = useChangedFiles( taskId, worktree, column, projectId, ); const [isMobile, setIsMobile] = useState(false); const [mobileView, setMobileView] = useState<"list" | "diff">("list"); // Track whether the user has manually navigated to the diff view on mobile. // This prevents the resize-to-mobile effect from stealing navigation intent. const mobileDiffIntentional = useRef(false); // Detect mobile viewport useEffect(() => { if (!isOpen) return; const checkMobile = () => { setIsMobile(window.innerWidth <= MOBILE_BREAKPOINT); }; checkMobile(); window.addEventListener("resize", checkMobile); return () => window.removeEventListener("resize", checkMobile); }, [isOpen]); // When resizing from desktop to mobile with a file selected, show diff pane // only if the user hasn't just opened the modal (which starts at the list). // When resizing from mobile to desktop, no special action needed (both panes visible) useEffect(() => { if (!isOpen || !isMobile) return; // Only auto-switch to diff on resize if the user is actively viewing a diff // (not the initial open where resetSelection has just cleared selectedFile) if (selectedFile && mobileDiffIntentional.current) { setMobileView("diff"); } }, [isOpen, isMobile, selectedFile]); // Auto-select first file on desktop when files load useEffect(() => { if (!isOpen || isMobile) return; if (!loading && files.length > 0 && !selectedFile) { setSelectedFile(files[0]); } }, [isOpen, isMobile, loading, files, selectedFile, setSelectedFile]); // Reset mobile view and selection when modal opens. // Always start on the file list so mobile users see changed files first. useEffect(() => { if (isOpen) { setMobileView("list"); mobileDiffIntentional.current = false; resetSelection(); } }, [isOpen, resetSelection]); // Escape key handler useEffect(() => { if (!isOpen) return; const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { // On mobile diff view, Escape goes back to list first if (isMobile && mobileView === "diff") { setMobileView("list"); } else { onClose(); } } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [isOpen, onClose, isMobile, mobileView]); const handleSelectFile = useCallback( (file: TaskFileDiff) => { setSelectedFile(file); if (isMobile) { mobileDiffIntentional.current = true; setMobileView("diff"); } }, [isMobile, setSelectedFile], ); const handleBackToList = useCallback(() => { setMobileView("list"); }, []); const selectedStat = useMemo( () => (selectedFile ? getDiffStat(selectedFile.diff) : ""), [selectedFile], ); if (!isOpen) return null; const sidebarClasses = [ "file-browser-sidebar", "changed-files-sidebar", isMobile ? "mobile" : "", isMobile && mobileView === "list" ? "active" : "", ] .filter(Boolean) .join(" "); const contentClasses = [ "file-browser-content", "changed-files-content", isMobile ? "mobile" : "", isMobile && mobileView === "diff" ? "active" : "", ] .filter(Boolean) .join(" "); const showBackButton = isMobile && mobileView === "diff"; return (
event.stopPropagation()} >
Changed Files — {taskId} {showBackButton && selectedFile ? ( {selectedFile.path} ) : null}
{selectedFile ? (
{showBackButton && ( )} {selectedFile.path} {getStatusLabel(selectedFile.status)} {selectedFile.oldPath ? ( Renamed from {selectedFile.oldPath} ) : null}
{selectedStat ?
{selectedStat}
: null}
                    {highlightDiff(selectedFile.diff || "No diff available")}
                  
) : !loading && !error && files.length > 0 ? (
Select a file to view changes
) : null}
); }