import React from 'react'; import { FileIcon } from '@renderer/components/team/editor/FileIcon'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { shortcutLabel } from '@renderer/utils/platformKeys'; import { ChevronDown, ChevronRight, FilePlus, GitBranch, Loader2, Save, Undo2 } from 'lucide-react'; import type { FileChangeWithContent, HunkDecision } from '@shared/types'; import type { FileChangeSummary } from '@shared/types/review'; const CONTENT_SOURCE_LABELS: Record = { 'ledger-exact': 'Task Ledger', 'ledger-snapshot': 'Ledger Snapshot', 'file-history': 'File History', 'snippet-reconstruction': 'Reconstructed', 'disk-current': 'Current Disk', 'git-fallback': 'Git Fallback', unavailable: 'Content unavailable', }; interface FileSectionHeaderProps { file: FileChangeSummary; fileContent: FileChangeWithContent | null; fileDecision: HunkDecision | undefined; externalChange?: { type: 'change' | 'add' | 'unlink' }; pathChangeLabel?: | { kind: 'deleted' } | { kind: 'moved' | 'renamed'; direction: 'from' | 'to'; otherPath: string }; hasEdits: boolean; applying: boolean; isCollapsed: boolean; onToggleCollapse: (filePath: string) => void; onDiscard: (filePath: string) => void; onSave: (filePath: string) => void; onReloadFromDisk?: (filePath: string) => void; onKeepDraft?: (filePath: string) => void; onRestoreMissingFile?: (filePath: string, content: string) => void; onAcceptFile?: (filePath: string) => void; onRejectFile?: (filePath: string) => void; } export const FileSectionHeader = ({ file, fileContent, fileDecision, externalChange, pathChangeLabel, hasEdits, applying, isCollapsed, onToggleCollapse, onDiscard, onSave, onReloadFromDisk, onKeepDraft, onRestoreMissingFile, onAcceptFile, onRejectFile, }: FileSectionHeaderProps): React.ReactElement => { const isMissingOnDisk = fileContent ? fileContent.modifiedFullContent == null : false; const isContentUnavailable = fileContent?.contentSource === 'unavailable'; const isPreviewOnly = isMissingOnDisk || isContentUnavailable; const requiresManualLedgerReview = file.snippets.some( (snippet) => !!snippet.ledger && (!!snippet.ledger.beforeState?.unavailableReason || !!snippet.ledger.afterState?.unavailableReason) && (snippet.ledger.originalFullContent == null || snippet.ledger.modifiedFullContent == null) ); const rejectDisabled = isPreviewOnly || requiresManualLedgerReview; const restoreContent = fileContent?.modifiedFullContent ?? (() => { const writeSnippets = file.snippets.filter( (s) => !s.isError && (s.type === 'write-new' || s.type === 'write-update') ); if (writeSnippets.length === 0) return null; return writeSnippets[writeSnippets.length - 1].newString; })(); const canRestore = !!onRestoreMissingFile && isMissingOnDisk && !isContentUnavailable && !hasEdits && restoreContent != null; const externalChangeLabel = externalChange?.type === 'unlink' ? 'Deleted on disk' : externalChange?.type === 'add' ? 'Recreated on disk' : externalChange?.type === 'change' ? 'Changed on disk' : null; const handleHeaderClick = (e: React.MouseEvent): void => { // Don't collapse when clicking action buttons if ((e.target as HTMLElement).closest('[data-no-collapse]')) return; onToggleCollapse(file.filePath); }; const handleHeaderKeyDown = (e: React.KeyboardEvent): void => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onToggleCollapse(file.filePath); } }; return (
{isCollapsed ? : } {file.relativePath} {file.isNewFile && ( NEW )} {pathChangeLabel?.kind === 'deleted' && ( DELETED )} {pathChangeLabel && pathChangeLabel.kind !== 'deleted' && ( {pathChangeLabel.kind.toUpperCase()} {pathChangeLabel.direction === 'from' ? 'From' : 'To'} {pathChangeLabel.otherPath} )} {fileContent?.contentSource && ( {isContentUnavailable ? 'Content unavailable' : isMissingOnDisk ? 'Missing on disk' : (CONTENT_SOURCE_LABELS[fileContent.contentSource] ?? fileContent.contentSource)} {isContentUnavailable ? (
Text content is unavailable
The ledger recorded metadata for this change, but full text content is not available. This usually means binary, large, or hash-only content.
Automatic accept/reject is disabled for this file to avoid unsafe disk writes.
) : isMissingOnDisk ? (
File is missing on disk
We can still show a preview from agent logs, but your filesystem is out of sync.
{restoreContent != null ? (
Use Restore to write the preview content back to disk.
) : (
Full file content is not available to restore automatically.
)}
) : ( {CONTENT_SOURCE_LABELS[fileContent.contentSource] ?? fileContent.contentSource} )}
)} {file.ledgerSummary?.worktreePath && ( WORKTREE
{file.ledgerSummary.worktreeBranch ?? 'Isolated worktree'}
{file.ledgerSummary.worktreePath}
{file.ledgerSummary.dirtyLeaderWarning && (
{file.ledgerSummary.dirtyLeaderWarning}
)}
)} {fileDecision && ( {fileDecision} )} {externalChangeLabel && ( {externalChangeLabel} )} {requiresManualLedgerReview && ( MANUAL REVIEW )}
{externalChange && onReloadFromDisk && onKeepDraft && (
)} {(onAcceptFile || onRejectFile) && (
{onAcceptFile && ( {isPreviewOnly && ( {isContentUnavailable ? 'Accept/Reject is disabled because full text content is unavailable.' : 'Accept/Reject is disabled while the file is missing on disk.'} )} )} {onRejectFile && ( {rejectDisabled && ( {requiresManualLedgerReview ? 'Reject is disabled because this ledger change has binary, large, or unavailable content.' : isContentUnavailable ? 'Reject is disabled because full text content is unavailable.' : 'Accept/Reject is disabled while the file is missing on disk.'} )} )}
)} {canRestore && restoreContent != null && ( Create/restore this file on disk from the preview )} {hasEdits && ( <> Discard all edits for this file Save file to disk {shortcutLabel('⌘ S', 'Ctrl+S')} )}
); };