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) { 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 [showManual, setShowManual] = useState(false);
const prevStateRef = useRef<string | null>(null); 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"; import { FileCard } from "./FileCard";
function getFileCommentMap( function getFileCommentMap(
@@ -13,7 +13,7 @@ function getFileCommentMap(
} }
interface DiffViewerProps { interface DiffViewerProps {
files: FileDiff[]; files: FileDiffDetail[];
commentsByLine: Map<string, ReviewComment[]>; commentsByLine: Map<string, ReviewComment[]>;
onAddComment: ( onAddComment: (
filePath: string, filePath: string,

View File

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

View File

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

View File

@@ -165,10 +165,12 @@ function getFileName(path: string): string {
return lastSlash >= 0 ? path.slice(lastSlash + 1) : path; return lastSlash >= 0 ? path.slice(lastSlash + 1) : path;
} }
const changeTypeDotColor: Record<string, string> = { const changeTypeDotColor: Record<string, string | undefined> = {
added: "bg-status-success-fg", added: "bg-status-success-fg",
deleted: "bg-status-error-fg", deleted: "bg-status-error-fg",
renamed: "bg-status-active-fg", renamed: "bg-status-active-fg",
modified: undefined,
binary: undefined,
}; };
function FilesView({ function FilesView({
@@ -310,7 +312,7 @@ function FilesView({
const isInView = activeFilePaths.has(file.newPath); const isInView = activeFilePaths.has(file.newPath);
const dimmed = selectedCommit && !isInView; const dimmed = selectedCommit && !isInView;
const isViewed = viewedFiles.has(file.newPath); const isViewed = viewedFiles.has(file.newPath);
const dotColor = changeTypeDotColor[file.changeType]; const dotColor = changeTypeDotColor[file.status];
return ( return (
<button <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[] { export function parseUnifiedDiff(raw: string): FileDiffDetail[] {
const files: FileDiff[] = []; const files: FileDiffDetail[] = [];
const fileChunks = raw.split(/^diff --git /m).filter(Boolean); const fileChunks = raw.split(/^diff --git /m).filter(Boolean);
for (const chunk of fileChunks) { for (const chunk of fileChunks) {
@@ -90,19 +90,19 @@ export function parseUnifiedDiff(raw: string): FileDiff[] {
hunks.push({ header, oldStart, oldCount, newStart, newCount, lines: hunkLines }); hunks.push({ header, oldStart, oldCount, newStart, newCount, lines: hunkLines });
} }
// Derive changeType from header markers and path comparison // Derive status from header markers and path comparison
let changeType: FileChangeType; let status: FileDiff['status'];
if (hasOldDevNull) { if (hasOldDevNull) {
changeType = "added"; status = "added";
} else if (hasNewDevNull) { } else if (hasNewDevNull) {
changeType = "deleted"; status = "deleted";
} else if (oldPath !== newPath) { } else if (oldPath !== newPath) {
changeType = "renamed"; status = "renamed";
} else { } else {
changeType = "modified"; status = "modified";
} }
files.push({ oldPath, newPath, hunks, additions, deletions, changeType }); files.push({ oldPath, newPath, hunks, additions, deletions, status });
} }
return files; 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; newLineNumber: number | null;
} }
export type FileChangeType = 'added' | 'modified' | 'deleted' | 'renamed'; /** Metadata returned by getPhaseReviewDiff — no hunk content */
export interface FileDiff { export interface FileDiff {
oldPath: string; oldPath: string;
newPath: string; newPath: string;
hunks: DiffHunk[]; /** 'binary' is new — prior changeType used FileChangeType which had no 'binary' */
status: 'added' | 'modified' | 'deleted' | 'renamed' | 'binary';
additions: number; additions: number;
deletions: 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 { export interface ReviewComment {