feat: split FileDiff into metadata FileDiff + hunk-bearing FileDiffDetail

Prepares the review components for the backend phase that returns
metadata-only file lists from getPhaseReviewDiff. FileDiff now holds
only path/status/additions/deletions; FileDiffDetail extends it with
hunks. Renames changeType→status and adds 'binary' to the union.

Also fixes two pre-existing TypeScript errors: InitiativeReview was
passing an unknown `comments` prop to DiffViewer (should be
commentsByLine), and ConflictResolutionPanel destructured an unused
`agent` variable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas May
2026-03-06 19:52:18 +01:00
parent 90978d631a
commit 5968a6ba88
8 changed files with 65 additions and 27 deletions

View File

@@ -11,7 +11,7 @@ interface ConflictResolutionPanelProps {
}
export function ConflictResolutionPanel({ initiativeId, conflicts, onResolved }: ConflictResolutionPanelProps) {
const { state, agent, questions, spawn, resume, stop, dismiss } = useConflictAgent(initiativeId);
const { state, agent: _agent, questions, spawn, resume, stop, dismiss } = useConflictAgent(initiativeId);
const [showManual, setShowManual] = useState(false);
const prevStateRef = useRef<string | null>(null);

View File

@@ -1,4 +1,4 @@
import type { FileDiff, DiffLine, ReviewComment } from "./types";
import type { FileDiffDetail, DiffLine, ReviewComment } from "./types";
import { FileCard } from "./FileCard";
function getFileCommentMap(
@@ -13,7 +13,7 @@ function getFileCommentMap(
}
interface DiffViewerProps {
files: FileDiff[];
files: FileDiffDetail[];
commentsByLine: Map<string, ReviewComment[]>;
onAddComment: (
filePath: string,

View File

@@ -8,12 +8,12 @@ import {
Circle,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import type { FileDiff, FileChangeType, DiffLine, ReviewComment } from "./types";
import type { FileDiffDetail, DiffLine, ReviewComment } from "./types";
import { HunkRows } from "./HunkRows";
import { useHighlightedFile } from "./use-syntax-highlight";
const changeTypeBadge: Record<
FileChangeType,
FileDiffDetail['status'],
{ label: string; classes: string } | null
> = {
added: {
@@ -32,17 +32,19 @@ const changeTypeBadge: Record<
"bg-status-active-bg text-status-active-fg border-status-active-border",
},
modified: null,
binary: null,
};
const leftBorderClass: Record<FileChangeType, string> = {
const leftBorderClass: Record<FileDiffDetail['status'], string> = {
added: "border-l-2 border-l-status-success-fg",
deleted: "border-l-2 border-l-status-error-fg",
renamed: "border-l-2 border-l-status-active-fg",
modified: "border-l-2 border-l-primary/40",
binary: "border-l-2 border-l-primary/40",
};
interface FileCardProps {
file: FileDiff;
file: FileDiffDetail;
commentsByLine: Map<string, ReviewComment[]>;
onAddComment: (
filePath: string,
@@ -80,7 +82,7 @@ export function FileCard({
[commentsByLine],
);
const badge = changeTypeBadge[file.changeType];
const badge = changeTypeBadge[file.status];
// Flatten all hunk lines for syntax highlighting
const allLines = useMemo(
@@ -93,7 +95,7 @@ export function FileCard({
<div className="rounded-lg border border-border overflow-clip">
{/* File header — sticky so it stays visible when scrolling */}
<button
className={`sticky z-10 flex w-full items-center gap-2 px-3 py-2 bg-muted hover:bg-muted/90 text-left text-sm font-mono transition-colors ${leftBorderClass[file.changeType]}`}
className={`sticky z-10 flex w-full items-center gap-2 px-3 py-2 bg-muted hover:bg-muted/90 text-left text-sm font-mono transition-colors ${leftBorderClass[file.status]}`}
style={{ top: 'var(--review-header-h, 0px)' }}
onClick={() => setExpanded(!expanded)}
>

View File

@@ -308,7 +308,7 @@ export function InitiativeReview({ initiativeId, onCompleted }: InitiativeReview
) : (
<DiffViewer
files={files}
comments={[]}
commentsByLine={new Map()}
onAddComment={() => {}}
onResolveComment={() => {}}
onUnresolveComment={() => {}}

View File

@@ -165,10 +165,12 @@ function getFileName(path: string): string {
return lastSlash >= 0 ? path.slice(lastSlash + 1) : path;
}
const changeTypeDotColor: Record<string, string> = {
const changeTypeDotColor: Record<string, string | undefined> = {
added: "bg-status-success-fg",
deleted: "bg-status-error-fg",
renamed: "bg-status-active-fg",
modified: undefined,
binary: undefined,
};
function FilesView({
@@ -310,7 +312,7 @@ function FilesView({
const isInView = activeFilePaths.has(file.newPath);
const dimmed = selectedCommit && !isInView;
const isViewed = viewedFiles.has(file.newPath);
const dotColor = changeTypeDotColor[file.changeType];
const dotColor = changeTypeDotColor[file.status];
return (
<button

View File

@@ -1,10 +1,10 @@
import type { FileDiff, FileChangeType, DiffHunk, DiffLine } from "./types";
import type { FileDiffDetail, FileDiff, DiffHunk, DiffLine } from "./types";
/**
* Parse a unified diff string into structured FileDiff objects.
* Parse a unified diff string into structured FileDiffDetail objects.
*/
export function parseUnifiedDiff(raw: string): FileDiff[] {
const files: FileDiff[] = [];
export function parseUnifiedDiff(raw: string): FileDiffDetail[] {
const files: FileDiffDetail[] = [];
const fileChunks = raw.split(/^diff --git /m).filter(Boolean);
for (const chunk of fileChunks) {
@@ -90,19 +90,19 @@ export function parseUnifiedDiff(raw: string): FileDiff[] {
hunks.push({ header, oldStart, oldCount, newStart, newCount, lines: hunkLines });
}
// Derive changeType from header markers and path comparison
let changeType: FileChangeType;
// Derive status from header markers and path comparison
let status: FileDiff['status'];
if (hasOldDevNull) {
changeType = "added";
status = "added";
} else if (hasNewDevNull) {
changeType = "deleted";
status = "deleted";
} else if (oldPath !== newPath) {
changeType = "renamed";
status = "renamed";
} else {
changeType = "modified";
status = "modified";
}
files.push({ oldPath, newPath, hunks, additions, deletions, changeType });
files.push({ oldPath, newPath, hunks, additions, deletions, status });
}
return files;

View File

@@ -0,0 +1,29 @@
// @vitest-environment happy-dom
import { describe, it, expect } from 'vitest';
import type { FileDiff, FileDiffDetail } from './types';
describe('FileDiff types', () => {
it('FileDiff accepts binary status', () => {
const f: FileDiff = {
oldPath: 'a.png',
newPath: 'a.png',
status: 'binary',
additions: 0,
deletions: 0,
};
expect(f.status).toBe('binary');
});
it('FileDiffDetail extends FileDiff with hunks', () => {
const d: FileDiffDetail = {
oldPath: 'a.ts',
newPath: 'a.ts',
status: 'modified',
additions: 5,
deletions: 2,
hunks: [],
};
expect(d.hunks).toEqual([]);
expect(d.additions).toBe(5);
});
});

View File

@@ -14,15 +14,20 @@ export interface DiffLine {
newLineNumber: number | null;
}
export type FileChangeType = 'added' | 'modified' | 'deleted' | 'renamed';
/** Metadata returned by getPhaseReviewDiff — no hunk content */
export interface FileDiff {
oldPath: string;
newPath: string;
hunks: DiffHunk[];
/** 'binary' is new — prior changeType used FileChangeType which had no 'binary' */
status: 'added' | 'modified' | 'deleted' | 'renamed' | 'binary';
additions: number;
deletions: number;
changeType: FileChangeType;
projectId?: string; // present in multi-project initiatives
}
/** Full diff with parsed hunks — returned by getFileDiff, parsed client-side */
export interface FileDiffDetail extends FileDiff {
hunks: DiffHunk[];
}
export interface ReviewComment {