In the previous chapter, Paginated File List, we built a scrollable menu allowing the user to select a file.
Now, the user presses Enter. They want to "zoom in" and see exactly what code changed.
You might think, "Easy! Just read the file and print it to the screen."
But files are dangerous:
We need a Detail View. Think of this component as a "Safe Document Viewer."
It performs two main jobs:
A Hunk is a technical term in Git. It represents a continuous block of changes.
Imagine editing a generic essay.
Your "Diff" contains 2 Hunks. We don't need to show Paragraphs 2, 3, or 4. We only render the Hunks to save space and focus attention.
Here is how the Detail View decides what to put on the screen.
We implement this in DiffDetailView.tsx. Let's break down the logic step-by-step.
Before we do any heavy lifting, we check the flags passed down from our data adapter.
// DiffDetailView.tsx
export function DiffDetailView({
filePath,
isBinary,
isLargeFile,
/* ... */
}: Props) {
// Safety Check 1: Binary Files
if (isBinary) {
return (
<Box flexDirection="column">
<Text bold>{filePath}</Text>
<Text dimColor italic>Binary file - cannot display diff</Text>
</Box>
);
}
// ... (Large file check looks similar)
If the file is binary, we return early. We display the filename (so the user knows where they are) and a dimmed warning message.
If the file is safe (text), we need to read it. Why? Because a "Diff" usually shows the change plus 3 lines of unchanged code around it for context. To show context, we need to read the actual file from the disk.
We use useMemo to ensure we only read the file when the filePath changes, not on every render frame.
// Read file content for syntax detection and context
const { firstLine, fileContent } = useMemo(() => {
if (!filePath) return { firstLine: null, fileContent: undefined };
const fullPath = resolve(getCwd(), filePath);
const content = readFileSafe(fullPath); // Helper to read disk
return {
firstLine: content?.split('\n')[0] ?? null,
fileContent: content ?? undefined,
};
}, [filePath]);
Terminals vary in size. If a line of code is too long, it might wrap uglily. We check the terminal width so we can truncate lines neatly.
// Get the current width of the terminal window
const { columns } = useTerminalSize();
// Calculate available space (minus borders/padding)
const availableWidth = columns - 2 - 2;
Finally, if the file is safe, we map over the hunks array.
We use a helper component called StructuredDiff. This helper handles the complex logic of coloring lines green (added) or red (removed) and highlighting syntax (like keywords in TypeScript).
return (
<Box flexDirection="column">
<Text bold>{filePath}</Text>
<Divider padding={4} />
{/* Loop through every change hunk */}
{hunks.map((hunk, index) => (
<StructuredDiff
key={index}
patch={hunk} // The raw change data
fileContent={fileContent} // For context
width={availableWidth} // For layout
/>
))}
</Box>
);
There is a special case: Untracked Files (New files not yet added to Git).
Git doesn't know line counts for untracked files yet. If we try to ask Git for "Hunks," it returns nothing. We need a specific UI state for this.
if (isUntracked) {
return (
<Box flexDirection="column">
<Text bold>{filePath}</Text>
<Divider padding={4} />
<Text dimColor italic>New file not yet staged.</Text>
<Text dimColor italic>
Run `git add {filePath}` to see line counts.
</Text>
</Box>
);
}
This guides the beginner user on why they can't see the code changes yet (they haven't staged the file).
Here is what happens when the Diff Dialog Orchestration switches to Detail Mode.
The Detail View & Hunk Rendering abstraction transforms raw data into a human-readable document.
StructuredDiff) to handle the coloring and formatting.We now have a fully functional Diff Viewer! We can list files and view their changes.
However, there is one final edge case. What happens if the file status changes while we are looking at it? Or if the data becomes invalid? We need one final layer of protection.
Next Chapter: File Status Guardrails
Generated by Code IQ